Implementation of CEP-55 - Generation of role names

patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20897
This commit is contained in:
Stefan Miklosovic 2025-09-16 15:17:53 +02:00
parent ed12f31f9d
commit 035a220d55
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
55 changed files with 2102 additions and 409 deletions

View File

@ -1,4 +1,5 @@
5.1 5.1
* Implementation of CEP-55 - Generation of role names (CASSANDRA-20897)
* Add cqlsh autocompletion for the identity mapping feature (CASSANDRA-20021) * Add cqlsh autocompletion for the identity mapping feature (CASSANDRA-20021)
* Add DDL Guardrail enabling administrators to disallow creation/modification of keyspaces with durable_writes = false (CASSANDRA-20913) * Add DDL Guardrail enabling administrators to disallow creation/modification of keyspaces with durable_writes = false (CASSANDRA-20913)
* Optimize Counter, Meter and Histogram metrics using thread local counters (CASSANDRA-20250) * Optimize Counter, Meter and Histogram metrics using thread local counters (CASSANDRA-20250)

View File

@ -102,10 +102,13 @@ New features
- Authentication mode is exposed in system_views.clients table, nodetool clientstats and ClientMetrics - Authentication mode is exposed in system_views.clients table, nodetool clientstats and ClientMetrics
to help operators identify which authentication modes are being used. nodetool clientstats introduces --verbose flag to help operators identify which authentication modes are being used. nodetool clientstats introduces --verbose flag
behind which this information is visible. behind which this information is visible.
- CEP-24 - Password validation / generation. When built-in 'password_validator' guardrail is enabled, it will - CEP-24 - Password validation / generation. When built-in 'password_policy' guardrail is enabled, it will
generate a password of configured password strength policy upon role creation or alteration generate a password of configured password strength policy upon role creation or alteration
when 'GENERATED PASSWORD' clause is used. Character sets supported are: English, Cyrillic, modern Cyrillic, when 'GENERATED PASSWORD' clause is used. Character sets supported are: English, Cyrillic, modern Cyrillic,
German, Polish and Czech. German, Polish and Czech.
- CEP-55 - Role name generation and validation. When built-in 'role_name_policy' guardrail is enabled, it will
generate a role name automatically, without operators intervention. It is possible to configure
this policy for both generation and as well as validation of role names.
- There is new MBean of name org.apache.cassandra.service.snapshot:type=SnapshotManager which exposes user-facing - There is new MBean of name org.apache.cassandra.service.snapshot:type=SnapshotManager which exposes user-facing
snapshot operations. Snapshot-related methods on StorageServiceMBean are still present and functional snapshot operations. Snapshot-related methods on StorageServiceMBean are still present and functional
but marked as deprecated. but marked as deprecated.

View File

@ -2507,20 +2507,41 @@ drop_compact_storage_enabled: false
# This would also apply to system keyspaces. # This would also apply to system keyspaces.
# maximum_replication_factor_warn_threshold: -1 # maximum_replication_factor_warn_threshold: -1
# maximum_replication_factor_fail_threshold: -1 # maximum_replication_factor_fail_threshold: -1
#
#role_name_policy:
# # Implementation class of a validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no validator.
# #validator_class_name: YourValidatorOfRoleNames
# # Implementation class of related generator which generates values which are valid when
# # tested against this validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no generator.
# # There is currently only one generator possible to configure - UUIDRoleNameGenerator
# generator_class_name: UUIDRoleNameGenerator
# # Minimum generated size of a name when name_size option is specified in CQL.
# # This can not be less than 10 and more than 32.
# #min_generated_name_size: 10
# If this is set to false, then it is not possible to call reconfiguration
# method in GuardrailsMBean. It will effectively forbid the reconfiguration of role_name_policy in runtime.
# You would need to stop the node, change the configuration in cassandra.yaml and start the node again.
# Defaults to true, which means that reconfiguration of role_name_policy via JMX is possible.
#role_name_policy_reconfiguration_enabled: true
# Guardrail to warn or fail when setting / altering a password. # Guardrail to warn or fail when setting / altering a password.
# Supported character sets are (both upper and lower-case): English, Cyrillic and modern Cyrillic, Czech, German, Polish. # Supported character sets are (both upper and lower-case): English, Cyrillic and modern Cyrillic, Czech, German, Polish.
# Password is invalid if all characters are from non-supported character set. If a password is otherwise valid, # Password is invalid if all characters are from non-supported character set. If a password is otherwise valid,
# but it contains characters from unsupported language, these characters contribute only to password length rule. # but it contains characters from unsupported language, these characters contribute only to password length rule.
# All digits and all following special characters are supported too: !"#$%&()*+,-./:;<=>?@[\]^_`{|}~ # All digits and all following special characters are supported too: !"#$%&()*+,-./:;<=>?@[\]^_`{|}~
#password_validator: #password_policy:
# # Implementation class of a validator. When not in form of FQCN, the # # Implementation class of a validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails.validators is prepended. # # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no validator. # # By default, there is no validator.
# class_name: CassandraPasswordValidator # validator_class_name: CassandraPasswordValidator
# # Implementation class of related generator which generates values which are valid when # # Implementation class of related generator which generates values which are valid when
# # tested against this validator. When not in form of FQCN, the # # tested against this validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails.generators is prepended. # # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no generator. # # By default, there is no generator.
# generator_class_name: CassandraPasswordGenerator # generator_class_name: CassandraPasswordGenerator
# # There are four characteristics: # # There are four characteristics:
@ -2571,7 +2592,7 @@ drop_compact_storage_enabled: false
# method in GuardrailsMBean. It will effectively forbid the reconfiguration of password validator in runtime. # method in GuardrailsMBean. It will effectively forbid the reconfiguration of password validator in runtime.
# You would need to stop the node, change the configuration in cassandra.yaml and start the node again. # You would need to stop the node, change the configuration in cassandra.yaml and start the node again.
# Defaults to true, which means that reconfiguration of password validator via JMX is possible. # Defaults to true, which means that reconfiguration of password validator via JMX is possible.
# password_validator_reconfiguration_enabled: true #password_policy_reconfiguration_enabled: true
# Guardrail to enable a CREATE or ALTER TABLE statement when default_time_to_live is set to 0 # Guardrail to enable a CREATE or ALTER TABLE statement when default_time_to_live is set to 0
# and the table is using TimeWindowCompactionStrategy compaction or a subclass of it. # and the table is using TimeWindowCompactionStrategy compaction or a subclass of it.

View File

@ -2290,6 +2290,98 @@ drop_compact_storage_enabled: false
# maximum_replication_factor_warn_threshold: -1 # maximum_replication_factor_warn_threshold: -1
# maximum_replication_factor_fail_threshold: -1 # maximum_replication_factor_fail_threshold: -1
#role_name_policy:
# # Implementation class of a validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no validator.
# #validator_class_name: YourValidatorOfRoleNames
# # Implementation class of related generator which generates values which are valid when
# # tested against this validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no generator.
# # There is currently only one generator possible to configure - UUIDRoleNameGenerator
# generator_class_name: UUIDRoleNameGenerator
# # Minimum generated size of a name when name_size option is specified in CQL.
# # This can not be less than 10 and more than 32.
# #min_generated_name_size: 10
# If this is set to false, then it is not possible to call reconfiguration
# method in GuardrailsMBean. It will effectively forbid the reconfiguration of role_name_policy in runtime.
# You would need to stop the node, change the configuration in cassandra.yaml and start the node again.
# Defaults to true, which means that reconfiguration of role_name_policy via JMX is possible.
#role_name_policy_reconfiguration_enabled: true
# If this is set to false, then it is not possible to call reconfiguration
# method in GuardrailsMBean. It will effectively forbid the reconfiguration of role_name_policy in runtime.
# You would need to stop the node, change the configuration in cassandra.yaml and start the node again.
# Defaults to true, which means that reconfiguration of role_name_policy via JMX is possible.
#rolename_validator_reconfiguration_enabled: true
# Guardrail to warn or fail when setting / altering a password.
# Supported character sets are (both upper and lower-case): English, Cyrillic and modern Cyrillic, Czech, German, Polish.
# Password is invalid if all characters are from non-supported character set. If a password is otherwise valid,
# but it contains characters from unsupported language, these characters contribute only to password length rule.
# All digits and all following special characters are supported too: !"#$%&()*+,-./:;<=>?@[\]^_`{|}~
#password_policy:
# # Implementation class of a validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no validator.
# validator_class_name: CassandraPasswordValidator
# # Implementation class of related generator which generates values which are valid when
# # tested against this validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no generator.
# generator_class_name: CassandraPasswordGenerator
# # There are four characteristics:
# # upper-case, lower-case, special character and digit.
# # If this value is set e.g. to 3, a password has to consist of 3 out of 4 characteristics.
# # For example, it has to contain at least 2 upper-case characters, 2 lower-case, and 2 digits to pass,
# # but it does not have to contain any special characters.
# # If number of characteristics found in the password is less than or equal to this number, it will emit warning.
# characteristic_warn: 3
# # If number of characteristics found in the password is less than or equal to this number, it will emit failure.
# characteristic_fail: 2
# # Maximum length of a password. Defaults to 1000.
# max_length: 1000
# # If password is shorter than this value, the validator will emit a warning.
# length_warn: 12
# # If a password is shorter than this value, the validator will emit a failure.
# length_fail: 8
# # If a password does not contain at least n upper-case characters, the validator will emit a warning.
# upper_case_warn: 2
# # If a password does not contain at least n upper-case characters, the validator will emit a failure.
# upper_case_fail: 1
# # If a password does not contain at least n lower-case characters, the validator will emit a warning.
# lower_case_warn: 2
# # If a password does not contain at least n lower-case characters, the validator will emit a failure.
# lower_case_fail: 1
# # If a password does not contain at least n digits, the validator will emit a warning.
# digit_warn: 2
# # If a password does not contain at least n digits, the validator will emit a failure.
# digit_fail: 1
# # If a password does not contain at least n special characters, the validator will emit a warning.
# special_warn: 2
# # If a password does not contain at least n special characters, the validator will emit a failure.
# special_fail: 1
# # If a password contain illegal sequences that at least this long, it is invalid.
# # Illegal sequences might be either alphabetical (form 'abcde'),
# # numerical (form '34567'), or US qwerty (form 'asdfg') as well as sequencies from supported character sets.
# # The minimum value for this property is 3, by default it is set to 5.
# illegal_sequence_length: 5
# # Dictionary to check the passwords against. Defaults to no dictionary.
# # Whole dictionary is cached into memory. Use with caution with relatively big dictionaries.
# # Entries in a dictionary, one per line, have to be sorted per String's compareTo contract.
# #dictionary: /path/to/dictionary/file
# # If set to true, a user will be informed what policies a suggested password is missing in order to be valid.
# # Defaults to true.
# detailed_messages: true
# If this is set to false, then it is not possible to call reconfiguration
# method in GuardrailsMBean. It will effectively forbid the reconfiguration of password validator in runtime.
# You would need to stop the node, change the configuration in cassandra.yaml and start the node again.
# Defaults to true, which means that reconfiguration of password validator via JMX is possible.
#password_policy_reconfiguration_enabled: true
# Guardrail to enable a CREATE or ALTER TABLE statement when default_time_to_live is set to 0 # Guardrail to enable a CREATE or ALTER TABLE statement when default_time_to_live is set to 0
# and the table is using TimeWindowCompactionStrategy compaction or a subclass of it. # and the table is using TimeWindowCompactionStrategy compaction or a subclass of it.
# It is suspicious to use default_time_to_live set to 0 with such compaction strategy. # It is suspicious to use default_time_to_live set to 0 with such compaction strategy.

View File

@ -1329,7 +1329,7 @@ h3(#createRoleStmt). CREATE ROLE
__Syntax:__ __Syntax:__
bc(syntax).. bc(syntax)..
<create-role-stmt> ::= CREATE ROLE ( IF NOT EXISTS )? <identifier> ( WITH <option> ( AND <option> )* )? <create-role-stmt> ::= CREATE (GENERATED)? ROLE ( IF NOT EXISTS )? <identifier> ( WITH <option> ( AND <option> )* )?
<option> ::= ("HASHED")? PASSWORD = <string> <option> ::= ("HASHED")? PASSWORD = <string>
| GENERATED PASSWORD | GENERATED PASSWORD
@ -1352,6 +1352,8 @@ CREATE ROLE carlos WITH OPTIONS = { 'custom_option1' : 'option1_value', 'custom_
CREATE ROLE rob WITH LOGIN = true and PASSWORD = 'password_c' AND ACCESS FROM ALL CIDRS; CREATE ROLE rob WITH LOGIN = true and PASSWORD = 'password_c' AND ACCESS FROM ALL CIDRS;
CREATE ROLE hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' }; CREATE ROLE hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
CREATE ROLE tom WITH LOGIN = true and GENERATED PASSWORD; CREATE ROLE tom WITH LOGIN = true and GENERATED PASSWORD;
CREATE GENERATED ROLE WITH GENERATED PASSWORD;
CREATE GENERATED ROLE WITH PASSWORD = 'password_e';
By default roles do not possess @LOGIN@ privileges or @SUPERUSER@ status. By default roles do not possess @LOGIN@ privileges or @SUPERUSER@ status.
@ -1370,9 +1372,13 @@ Use the @WITH PASSWORD@ clause to set a password for internal authentication, en
If internal authentication has not been set up or the role does not have @LOGIN@ privileges, the @WITH PASSWORD@ clause is not necessary. If internal authentication has not been set up or the role does not have @LOGIN@ privileges, the @WITH PASSWORD@ clause is not necessary.
When @WITH GENERATED PASSWORD@ is used, Cassandra provides out-of-the-box CassandraPasswordValidator and CassandraPasswordGenerator When @WITH GENERATED PASSWORD@ is used, Cassandra provides out-of-the-box CassandraPasswordValidator and CassandraPasswordGenerator
under "password_validator" configuration property in cassandra.yaml. The usage of this clause will generate a password for a given password strength policy, as configured, under "password_policy" configuration property in cassandra.yaml. The usage of this clause will generate a password for a given password strength policy, as configured,
and such password is returned to a client in CQL shell after query is executed. @GENERATED PASSWORD@ can not be used together with @HASHED PASSWORD@ nor with @PASSWORD@ alone. and such password is returned to a client in CQL shell after query is executed. @GENERATED PASSWORD@ can not be used together with @HASHED PASSWORD@ nor with @PASSWORD@ alone.
When @GENERATED@ is used in front of @ROLE@, it is possible to generate a role name automatically. This is possible if `role_name_policy` is enabled in the configuration.
The usage of this clause will generate a role name based on given `generator_class_name` in `role_name_policy` section in cassandra.yaml. Generated role name (together with generated password) will
be returned in CQL response to a caller. Custom implementations of `IRoleManager` are able to specify custom response to return to a caller if default behaviour is not desired.
h4(#createRoleConditional). Creating a role conditionally h4(#createRoleConditional). Creating a role conditionally
Attempting to create an existing role results in an invalid query condition unless the @IF NOT EXISTS@ option is used. If the option is used and the role exists, the statement is a no-op. Attempting to create an existing role results in an invalid query condition unless the @IF NOT EXISTS@ option is used. If the option is used and the role exists, the statement is a no-op.
@ -1522,7 +1528,6 @@ bc(syntax)..
<create-user-statement> ::= CREATE USER ( IF NOT EXISTS )? <identifier> ( WITH <option> ( AND <option> )* )? <create-user-statement> ::= CREATE USER ( IF NOT EXISTS )? <identifier> ( WITH <option> ( AND <option> )* )?
<option> ::= ("HASHED")? PASSWORD = <string> <option> ::= ("HASHED")? PASSWORD = <string>
| GENERATED PASSWORD
| SUPERUSER | SUPERUSER
| NOSUPERUSER | NOSUPERUSER
p. p.
@ -1532,7 +1537,6 @@ __Sample:__
bc(sample). bc(sample).
CREATE USER alice WITH PASSWORD 'password_a' SUPERUSER; CREATE USER alice WITH PASSWORD 'password_a' SUPERUSER;
CREATE USER bob WITH PASSWORD 'password_b' NOSUPERUSER; CREATE USER bob WITH PASSWORD 'password_b' NOSUPERUSER;
CREATE USER tom WITH GENERATED PASSWORD;
@CREATE USER@ is equivalent to @CREATE ROLE@ where the @LOGIN@ option is @true@. So, the following pairs of statements are equivalent: @CREATE USER@ is equivalent to @CREATE ROLE@ where the @LOGIN@ option is @true@. So, the following pairs of statements are equivalent:
@ -1561,7 +1565,6 @@ bc(syntax)..
<alter-user-statement> ::= ALTER USER (IF EXISTS)? <identifier> ( WITH <option> ( AND <option> )* )? <alter-user-statement> ::= ALTER USER (IF EXISTS)? <identifier> ( WITH <option> ( AND <option> )* )?
<option> ::= ("HASHED")? PASSWORD = <string> <option> ::= ("HASHED")? PASSWORD = <string>
| GENERATED PASSWORD
| SUPERUSER | SUPERUSER
| NOSUPERUSER | NOSUPERUSER
p. p.
@ -2729,6 +2732,7 @@ h3. 3.4.8
* Add support for the BETWEEN operator in WHERE clauses (see "CASSANDRA-19604":https://issues.apache.org/jira/browse/CASSANDRA-19604) * Add support for the BETWEEN operator in WHERE clauses (see "CASSANDRA-19604":https://issues.apache.org/jira/browse/CASSANDRA-19604)
* Add support for GENERATED PASSWORD clause (see "CASSANDRA-17457":https://issues.apache.org/jira/browse/CASSANDRA-17457) * Add support for GENERATED PASSWORD clause (see "CASSANDRA-17457":https://issues.apache.org/jira/browse/CASSANDRA-17457)
* Add support for GENERATED ROLE clause (see "CASSANDRA-17457":https://issues.apache.org/jira/browse/CASSANDRA-20902)
h3. 3.4.7 h3. 3.4.7

View File

@ -110,6 +110,7 @@
**** xref:cassandra:managing/operating/transientreplication.adoc[Transient replication] **** xref:cassandra:managing/operating/transientreplication.adoc[Transient replication]
**** xref:cassandra:managing/operating/virtualtables.adoc[Virtual tables] **** xref:cassandra:managing/operating/virtualtables.adoc[Virtual tables]
**** xref:cassandra:managing/operating/password_validation.adoc[Password validation] **** xref:cassandra:managing/operating/password_validation.adoc[Password validation]
**** xref:cassandra:managing/operating/role_name_generation.adoc[Role name generation]
**** xref:cassandra:managing/operating/onboarding-to-accord.adoc[] **** xref:cassandra:managing/operating/onboarding-to-accord.adoc[]
*** xref:cassandra:managing/tools/index.adoc[Tools] *** xref:cassandra:managing/tools/index.adoc[Tools]
**** xref:cassandra:managing/tools/cqlsh.adoc[cqlsh: the CQL shell] **** xref:cassandra:managing/tools/cqlsh.adoc[cqlsh: the CQL shell]

View File

@ -20,4 +20,5 @@
* xref:cassandra:managing/operating/transientreplication.adoc[Transient replication] * xref:cassandra:managing/operating/transientreplication.adoc[Transient replication]
* xref:cassandra:managing/operating/virtualtables.adoc[Virtual tables] * xref:cassandra:managing/operating/virtualtables.adoc[Virtual tables]
* xref:cassandra:managing/operating/password_validation.adoc[Password validation] * xref:cassandra:managing/operating/password_validation.adoc[Password validation]
* xref:cassandra:managing/operating/role_name_generation.adoc[Role name generation]
* xref:cassandra:managing/operating/onboarding-to-accord.adoc[] * xref:cassandra:managing/operating/onboarding-to-accord.adoc[]

View File

@ -50,7 +50,7 @@ the password guardrail provides `CassandraPasswordValidator` by extending `Value
while passwords are generated by `CassandraPasswordGenerator` by extending `ValueGenerator`. while passwords are generated by `CassandraPasswordGenerator` by extending `ValueGenerator`.
Both components work with passwords as `String` type values. Both components work with passwords as `String` type values.
Password validation and generation are configured in `cassandra.yaml` file under the `password_validator` section. Password validation and generation are configured in `cassandra.yaml` file under the `password_policy` section.
Lets explore the key configuration properties available. Lets explore the key configuration properties available.
First, the `class_name` and `generator_class_name` parameters First, the `class_name` and `generator_class_name` parameters
@ -66,7 +66,7 @@ However, the framework is designed with flexibility in mind, allowing organizati
and generation rules that align with their specific security policies and business requirements. and generation rules that align with their specific security policies and business requirements.
---- ----
password_validator: password_policy:
# Implementation class of a validator. When not in form of FQCN, the # Implementation class of a validator. When not in form of FQCN, the
# package name org.apache.cassandra.db.guardrails.validators is prepended. # package name org.apache.cassandra.db.guardrails.validators is prepended.
# By default, there is no validator. # By default, there is no validator.
@ -243,12 +243,6 @@ We can create a role with generated password like this:
cassandra@cqlsh> CREATE ROLE alice WITH GENERATED PASSWORD AND LOGIN = true; cassandra@cqlsh> CREATE ROLE alice WITH GENERATED PASSWORD AND LOGIN = true;
---- ----
or by `CREATE USER`:
----
cassandra@cqlsh> CREATE USER alice WITH GENERATED PASSWORD;
----
When a password is generated for `alice` she can log in: When a password is generated for `alice` she can log in:
---- ----
@ -274,7 +268,7 @@ classname = PlainTextAuthProvider
---- ----
It is also possible to configure password validators in such a way that a user does not see why a password failed. It is also possible to configure password validators in such a way that a user does not see why a password failed.
This is driven by configuration property for `password_validator` called `detailed_messages`. When set to `false`, This is driven by configuration property for `password_policy` called `detailed_messages`. When set to `false`,
the violations will be very brief: the violations will be very brief:
---- ----
@ -299,11 +293,12 @@ implementation and may be addressed in future updates.
Since this solution is based on guardrails which are configurable via JMX in runtime, same hold for Since this solution is based on guardrails which are configurable via JMX in runtime, same hold for
password validator, also configured via `GuardrailsMBean` as any other guardrails. There are two methods exposed: password validator, also configured via `GuardrailsMBean` as any other guardrails. There are two methods exposed:
* `Map<String, Object> getPasswordValidatorConfig()` - gets password validator configuration * `Map<String, Object> getPasswordPolicy()` - gets password validator configuration as JSON string,
* `void reconfigurePasswordValidator(Map<String, Object> config)` - reconfigures the password validator by reading * `void setPasswordPolicy(String value)` - reconfigures the password validator by reading
and parsing the configuration from the provided map. Reconfiguration of password validator in runtime is considered and parsing the configuration from the provided String, being JSON representation of configuration map.
Reconfiguration of password validator in runtime is considered
to be very sensitive operation. If an operator evaluates the reconfiguration in runtime is not allowed, they to be very sensitive operation. If an operator evaluates the reconfiguration in runtime is not allowed, they
might set `password_validator_reconfiguration_enabled` to `false` in `cassandra.yaml` to disable it. might set `password_policy_reconfiguration_enabled` to `false` in `cassandra.yaml` to disable it.
=== Diagnostic events === Diagnostic events

View File

@ -0,0 +1,122 @@
= Role name validation and generation
:navtitle: Role name generation and validation
:description: Role name generation and validation - How it works, how to configure it, and more.
:keywords: CEP-55, User, Role, Generation, Validation, Security
CEP-55 is logical continuation of CEP-24. CEP-24 covers generation and validation of passwords while CEP-55 deals with role name generation.
The configuration of role name policy in cassandra.yaml is as follows:
----
role_name_policy:
# # Implementation class of a validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no validator.
# #validator_class_name: YourValidatorOfRoleNames
# # Implementation class of related generator which generates values which are valid when
# # tested against this validator. When not in form of FQCN, the
# # package name org.apache.cassandra.db.guardrails is prepended.
# # By default, there is no generator.
# # There is currently only one generator possible to configure - UUIDRoleNameGenerator
generator_class_name: UUIDRoleNameGenerator
# # Minimum generated size of a name when name_size option is specified in CQL.
# # This can not be less than 10 and more than 32.
# #min_generated_name_size: 10
----
`UUIDRoleNameGenerator` is the only built-in implementation of a generator which generates role names. As
its name suggests, generated role names will be UUIDs (without hyphens) and their first character will be always a letter.
Once enabled (together with `CassandraRoleManager` as `role_manager`), a user might execute these queries:
The most simple variation is:
----
CREATE GENERATED ROLE;
generated_role_name
----------------------------------
a40dcc81c5964a79a5be670d99b18159
----
Every generation command will return a result resembling a table which will return `generated_role_name` value to a user.
----
CREATE GENERATED ROLE WITH PASSWORD = 'U9~63o<7M}oM';
generated_role_name
----------------------------------
f7f1871a41d740b483ac561cbbf1e4a9
----
When CEP-24 / password generation is enabled, it is possible to combine the generation of a role name together with
the generation of a password like this:
----
CREATE GENERATED ROLE WITH GENERATED PASSWORD;
generated_password | generated_role_name
--------------------+----------------------------------
0zdHJ,]dW[s6 | cbaeea62551d41dbb26a349428ccd741
----
If we want that role to be able to log in, we would execute:
----
CREATE GENERATED ROLE WITH GENERATED PASSWORD AND LOGIN = true;
----
It is also possible to specify custom prefix and suffix of a generated role name like this:
----
CREATE GENERATED ROLE WITH OPTIONS = {'name_suffix': '_read_only'};
generated_role_name
--------------------------------------------
b40cc1a568b142e1b5630a9083880aa8_read_only
----
or prefix version:
----
CREATE GENERATED ROLE WITH OPTIONS = {'name_prefix': 'department_a_'};
generated_role_name
----------------------------------------------------
department_a_d7cc48cac819456fa3eb38c59649ec60
----
Of course, it is also possible to specify both `name_prefix` and `name_suffix` at the same time.
It is also possible to specify length of generated role name via `name_size` parameter:
----
CREATE GENERATED ROLE WITH OPTIONS = {'name_suffix': '_read_only', 'name_prefix': 'department_a_', 'name_size': 15};
generated_role_name
----------------------------------------------------
department_a_ac7896b4994d40c_read_only
----
`min_generated_name_size` parameter in `cassandra.yaml` sets minimum size of name.
If `name_size` is specified in CQL's OPTIONS, it will be checked against this minimum size and
fail if smaller than that. The value of `name_size` option can not be less than 10 and more than 32.
There is currently no implementation of `validator_class_name` you might use. However, if you happen to
put your implementation on a class path of Cassandra and referenced it in `validator_class_name` in `role_name_policy`,
if somebody creates a role by specifying a role name by hand (or generated), it will be validated against such validator and either a warning will be issued to a caller or it will fail the query altogether, depending based on what logic it implemented. When used together with generator, it will basically validate what it generated to
comply with its self-imposed policy. That way it will fail if you generate a role name in such a way that it would
fail role creation if a user happened to specify it manually.
The configuration of `role_name_policy` is configurable in runtime via JMX. You get the actual configuration by
calling `GuardrailsMBean#getRoleNamePolicy()` which returns JSON representation of such a policy as a string. Similarly,
you can use `GuardrailsMBean#setRoleNamePolicy(String)` to configure it in runtime.
If you do not want to be able to configure this in runtime, please set `role_name_policy_reconfiguration_enabled`
in `cassandra.yaml` to `false`.
The fact that you see a "table" returned after generated role name / password is the result of `CassandraRoleManager`
overriding `createRoleWithResult` and `alterRoleWithResult` methods and populating the response message with
generated values. If you happen to have a completely custom implementation of `IRoleManager`, e.g. integrating with
an external service, this is a way how to hook in your own "credentials" to be returned to a user.
E.g. an `IRoleManager` integrating with some vault-like solution might return there custom credentials-like values for end-user to act on and similar.

View File

@ -393,12 +393,18 @@ See also: xref:cassandra:developing/cql/security.adoc#grant-permission[`GRANT PE
xref:cassandra:developing/cql/security.adoc#grant-all[`GRANT ALL`] and xref:cassandra:developing/cql/security.adoc#grant-all[`GRANT ALL`] and
xref:cassandra:developing/cql/security.adoc#revoke-permission[`REVOKE PERMISSION`]. xref:cassandra:developing/cql/security.adoc#revoke-permission[`REVOKE PERMISSION`].
== Password validation == Password generation and validation
If you are interested into the application of a certain security policy for password strength for If you are interested into the application of a certain security policy for password strength for
user passwords, you are welcome to read about it more in xref:cassandra:managing/operating/password_validation.adoc[here] user passwords, you are welcome to read about it more in xref:cassandra:managing/operating/password_validation.adoc[here]
which implements https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=228494146[CEP-24]. which implements https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=228494146[CEP-24].
== Role name generation and validation
It is possible to automatically generate role names and validate them upon role or user creation. This feature
was implemented in https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-55+Generated+role+names[CEP-55].
You can read more about this feature in depth xref:cassandra:managing/operating/role_name_generation.adoc[here].
[[auth-caching]] [[auth-caching]]
== Caching == Caching

View File

@ -53,7 +53,8 @@ COMPLEX_OPTIONS = (
'transparent_data_encryption_options', 'transparent_data_encryption_options',
'hinted_handoff_disabled_datacenters', 'hinted_handoff_disabled_datacenters',
'startup_checks', 'startup_checks',
'password_validator' 'password_policy',
'role_name_policy'
) )
def convert(yaml_file, dest_file): def convert(yaml_file, dest_file):

View File

@ -305,6 +305,7 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
| <alterUserStatement> | <alterUserStatement>
| <dropUserStatement> | <dropUserStatement>
| <listUsersStatement> | <listUsersStatement>
| <createGeneratedRoleStatement>
| <createRoleStatement> | <createRoleStatement>
| <alterRoleStatement> | <alterRoleStatement>
| <dropRoleStatement> | <dropRoleStatement>
@ -1619,6 +1620,10 @@ syntax_rules += r'''
| <unreservedKeyword> ) | <unreservedKeyword> )
; ;
<createGeneratedRoleStatement> ::= "CREATE" "GENERATED" "ROLE"
( "WITH" <roleProperty> ("AND" <roleProperty>)*)?
;
<createRoleStatement> ::= "CREATE" "ROLE" ("IF" "NOT" "EXISTS")? <rolename> <createRoleStatement> ::= "CREATE" "ROLE" ("IF" "NOT" "EXISTS")? <rolename>
( "WITH" <roleProperty> ("AND" <roleProperty>)*)? ( "WITH" <roleProperty> ("AND" <roleProperty>)*)?
; ;

View File

@ -540,7 +540,7 @@ class Shell(cmd.Cmd):
ksname = self.current_keyspace ksname = self.current_keyspace
ksmeta = self.get_keyspace_meta(ksname) ksmeta = self.get_keyspace_meta(ksname)
if tablename not in ksmeta.tables: if tablename not in ksmeta.tables:
if ksname == 'system_auth' and tablename in ['roles', 'role_permissions', 'generated_password']: if ksname == 'system_auth' and tablename in ['roles', 'role_permissions', 'generated_values']:
self.get_fake_auth_table_meta(ksname, tablename) self.get_fake_auth_table_meta(ksname, tablename)
else: else:
raise ColumnFamilyNotFound("Column family {} not found".format(tablename)) raise ColumnFamilyNotFound("Column family {} not found".format(tablename))
@ -563,10 +563,11 @@ class Shell(cmd.Cmd):
table_meta.columns['role'] = ColumnMetadata(table_meta, 'role', cassandra.cqltypes.UTF8Type) table_meta.columns['role'] = ColumnMetadata(table_meta, 'role', cassandra.cqltypes.UTF8Type)
table_meta.columns['resource'] = ColumnMetadata(table_meta, 'resource', cassandra.cqltypes.UTF8Type) table_meta.columns['resource'] = ColumnMetadata(table_meta, 'resource', cassandra.cqltypes.UTF8Type)
table_meta.columns['permission'] = ColumnMetadata(table_meta, 'permission', cassandra.cqltypes.UTF8Type) table_meta.columns['permission'] = ColumnMetadata(table_meta, 'permission', cassandra.cqltypes.UTF8Type)
elif tablename == 'generated_password': elif tablename == 'generated_values':
ks_meta = KeyspaceMetadata(ksname, True, None, None) ks_meta = KeyspaceMetadata(ksname, True, None, None)
table_meta = TableMetadata(ks_meta, 'generated_password') table_meta = TableMetadata(ks_meta, 'generated_values')
table_meta.columns['generated_password'] = ColumnMetadata(table_meta, 'generated_password', cassandra.cqltypes.UTF8Type) table_meta.columns['generated_password'] = ColumnMetadata(table_meta, 'generated_password', cassandra.cqltypes.UTF8Type)
table_meta.columns['generated_role_name'] = ColumnMetadata(table_meta, 'generated_role_name', cassandra.cqltypes.UTF8Type)
else: else:
raise ColumnFamilyNotFound("Column family {} not found".format(tablename)) raise ColumnFamilyNotFound("Column family {} not found".format(tablename))
@ -911,16 +912,18 @@ class Shell(cmd.Cmd):
if result is None: if result is None:
return False, None return False, None
lowered_query = statement.query_string.lower()
if statement.query_string[:6].lower() == 'select': if statement.query_string[:6].lower() == 'select':
self.print_result(result, self.parse_for_select_meta(statement.query_string)) self.print_result(result, self.parse_for_select_meta(statement.query_string))
elif statement.query_string.lower().startswith("list users") or statement.query_string.lower().startswith("list roles"): elif lowered_query.startswith("list users") or lowered_query.startswith("list roles"):
self.print_result(result, self.get_table_meta('system_auth', 'roles')) self.print_result(result, self.get_table_meta('system_auth', 'roles'))
elif statement.query_string.lower().startswith("list"): elif lowered_query.startswith("list"):
self.print_result(result, self.get_table_meta('system_auth', 'role_permissions')) self.print_result(result, self.get_table_meta('system_auth', 'role_permissions'))
elif statement.query_string.lower().startswith("create user") or statement.query_string.lower().startswith("create role"): elif lowered_query.startswith("create role") or lowered_query.startswith("create generated role"):
self.print_result(result, self.get_table_meta('system_auth', 'generated_password')) self.print_result(result, self.get_table_meta('system_auth', 'generated_values'))
elif statement.query_string.lower().startswith("alter user") or statement.query_string.lower().startswith("alter role"): elif lowered_query.startswith("alter role"):
self.print_result(result, self.get_table_meta('system_auth', 'generated_password')) self.print_result(result, self.get_table_meta('system_auth', 'generated_values'))
elif result: elif result:
# CAS INSERT/UPDATE # CAS INSERT/UPDATE
self.writeresult("") self.writeresult("")

View File

@ -1187,6 +1187,9 @@ class TestCqlshCompletion(CqlshCompletionCase):
def test_complete_in_alter_user(self): def test_complete_in_alter_user(self):
self.trycompletions('ALTER USER ', choices=['<identifier>', 'IF', '<pgStringLiteral>', '<quotedStringLiteral>']) self.trycompletions('ALTER USER ', choices=['<identifier>', 'IF', '<pgStringLiteral>', '<quotedStringLiteral>'])
def test_complete_in_generated(self):
self.trycompletions('CREATE GENERATED ', immediate='ROLE ')
def test_complete_in_create_role(self): def test_complete_in_create_role(self):
self.trycompletions('CREATE ROLE ', choices=['<rolename>', 'IF']) self.trycompletions('CREATE ROLE ', choices=['<rolename>', 'IF'])
self.trycompletions('CREATE ROLE IF ', immediate='NOT EXISTS ') self.trycompletions('CREATE ROLE IF ', immediate='NOT EXISTS ')
@ -1196,6 +1199,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
self.trycompletions('CREATE ROLE foo WITH ACCESS TO ALL ', immediate='DATACENTERS ') self.trycompletions('CREATE ROLE foo WITH ACCESS TO ALL ', immediate='DATACENTERS ')
self.trycompletions('CREATE ROLE foo WITH ACCESS FROM ', choices=['ALL', 'CIDRS']) self.trycompletions('CREATE ROLE foo WITH ACCESS FROM ', choices=['ALL', 'CIDRS'])
self.trycompletions('CREATE ROLE foo WITH ACCESS FROM ALL ', immediate='CIDRS ') self.trycompletions('CREATE ROLE foo WITH ACCESS FROM ALL ', immediate='CIDRS ')
self.trycompletions('CREATE GENERATED ROLE WITH ', choices=['ACCESS', 'HASHED', 'LOGIN', 'OPTIONS', 'PASSWORD', 'SUPERUSER', 'GENERATED'])
def test_complete_in_alter_role(self): def test_complete_in_alter_role(self):
self.trycompletions('ALTER ROLE ', choices=['<identifier>', 'IF', '<quotedName>']) self.trycompletions('ALTER ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])

View File

@ -1453,15 +1453,8 @@ createUserStatement returns [CreateRoleStatement stmt]
{ {
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive"); throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
} }
if (opts.getPassword().isPresent() && opts.isGeneratedPassword()) $stmt = new CreateRoleStatement(name, opts, DCPermissions.all(), CIDRPermissions.all(), ifNotExists);
{ }
throw new SyntaxException("Options 'password' and 'generated password' are mutually exclusive");
}
if (opts.getHashedPassword().isPresent() && opts.isGeneratedPassword())
{
throw new SyntaxException("Options 'hashed password' and 'generated password' are mutually exclusive");
}
$stmt = new CreateRoleStatement(name, opts, DCPermissions.all(), CIDRPermissions.all(), ifNotExists); }
; ;
/** /**
@ -1535,10 +1528,11 @@ listUsersStatement returns [ListRolesStatement stmt]
; ;
/** /**
* CREATE ROLE [IF NOT EXISTS] <rolename> [ [WITH] option [ [AND] option ]* ] * CREATE [GENERATED] ROLE [IF NOT EXISTS] <rolename> [ [WITH] option [ [AND] option ]* ]
* *
* where option can be: * where option can be:
* PASSWORD = '<password>' * PASSWORD = '<password>'
* GENERATED PASSWORD
* SUPERUSER = (true|false) * SUPERUSER = (true|false)
* LOGIN = (true|false) * LOGIN = (true|false)
* OPTIONS = { 'k1':'v1', 'k2':'v2'} * OPTIONS = { 'k1':'v1', 'k2':'v2'}
@ -1553,8 +1547,9 @@ createRoleStatement returns [CreateRoleStatement stmt]
DCPermissions.Builder dcperms = DCPermissions.builder(); DCPermissions.Builder dcperms = DCPermissions.builder();
CIDRPermissions.Builder cidrperms = CIDRPermissions.builder(); CIDRPermissions.Builder cidrperms = CIDRPermissions.builder();
boolean ifNotExists = false; boolean ifNotExists = false;
boolean isGeneratedName = false;
} }
: K_CREATE K_ROLE (K_IF K_NOT K_EXISTS { ifNotExists = true; })? name=userOrRoleName : K_CREATE (K_GENERATED { isGeneratedName = true; })? K_ROLE (K_IF K_NOT K_EXISTS { ifNotExists = true; })? (name=userOrRoleName)?
( K_WITH roleOptions[opts, dcperms, cidrperms] )? ( K_WITH roleOptions[opts, dcperms, cidrperms] )?
{ {
// set defaults if they weren't explictly supplied // set defaults if they weren't explictly supplied
@ -1578,6 +1573,18 @@ createRoleStatement returns [CreateRoleStatement stmt]
{ {
throw new SyntaxException("Options 'hashed password' and 'generated password' are mutually exclusive"); throw new SyntaxException("Options 'hashed password' and 'generated password' are mutually exclusive");
} }
if (isGeneratedName)
{
if (name != null)
{
throw new SyntaxException("Name can not be specified together with GENERATED keyword.");
}
if (ifNotExists)
{
throw new SyntaxException("GENERATED keyword for role creation can not be used together with IF NOT EXISTS.");
}
opts.setOption(IRoleManager.Option.GENERATED_NAME, true);
}
$stmt = new CreateRoleStatement(name, opts, dcperms.build(), cidrperms.build(), ifNotExists); $stmt = new CreateRoleStatement(name, opts, dcperms.build(), cidrperms.build(), ifNotExists);
} }
; ;

View File

@ -51,7 +51,7 @@ import org.apache.cassandra.cql3.PasswordObfuscator;
import org.apache.cassandra.cql3.QueryEvents; import org.apache.cassandra.cql3.QueryEvents;
import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.statements.BatchStatement; import org.apache.cassandra.cql3.statements.BatchStatement;
import org.apache.cassandra.db.guardrails.PasswordGuardrail; import org.apache.cassandra.db.guardrails.PasswordPolicyGuardrail;
import org.apache.cassandra.exceptions.AuthenticationException; import org.apache.cassandra.exceptions.AuthenticationException;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.PreparedQueryNotFoundException; import org.apache.cassandra.exceptions.PreparedQueryNotFoundException;
@ -414,9 +414,9 @@ public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listene
return "Syntax Exception. Obscured for security reasons."; return "Syntax Exception. Obscured for security reasons.";
} }
} }
else if (e instanceof PasswordGuardrail.PasswordGuardrailException) else if (e instanceof PasswordPolicyGuardrail.PasswordGuardrailException)
{ {
return ((PasswordGuardrail.PasswordGuardrailException) e).redactedMessage; return ((PasswordPolicyGuardrail.PasswordGuardrailException) e).redactedMessage;
} }
return PasswordObfuscator.obfuscate(e.getMessage()); return PasswordObfuscator.obfuscate(e.getMessage());

View File

@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -51,11 +52,16 @@ import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.DurationSpec; import org.apache.cassandra.config.DurationSpec;
import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.ResultSet;
import org.apache.cassandra.cql3.UntypedResultSet; import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.statements.SelectStatement; import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.guardrails.NoOpGenerator;
import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.InvalidRequestException;
@ -76,6 +82,7 @@ import org.apache.cassandra.utils.NoSpamLogger;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
import static org.apache.cassandra.service.QueryState.forInternalCalls; import static org.apache.cassandra.service.QueryState.forInternalCalls;
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
/** /**
* Responsible for the creation, maintenance and deletion of roles * Responsible for the creation, maintenance and deletion of roles
@ -164,9 +171,14 @@ public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerM
public CassandraRoleManager(Map<String, String> parameters) public CassandraRoleManager(Map<String, String> parameters)
{ {
supportedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator Set<Option> allowedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD) ? EnumSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD, Option.GENERATED_NAME)
: ImmutableSet.of(Option.LOGIN, Option.SUPERUSER); : EnumSet.of(Option.LOGIN, Option.SUPERUSER);
if (Guardrails.roleNamePolicy.getGenerator() != NoOpGenerator.INSTANCE)
allowedOptions.add(Option.OPTIONS);
supportedOptions = ImmutableSet.copyOf(allowedOptions);
alterableOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator alterableOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
? ImmutableSet.of(Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD) ? ImmutableSet.of(Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD)
: ImmutableSet.<Option>of(); : ImmutableSet.<Option>of();
@ -340,6 +352,20 @@ public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerM
} }
} }
@Override
public ResultMessage alterRoleWithResult(AuthenticatedUser performer, RoleResource role, RoleOptions options)
{
alterRole(performer, role, options);
return getResultMessageForRoleCreatedOrAltered(role, options);
}
@Override
public ResultMessage createRoleWithResult(AuthenticatedUser performer, RoleResource role, RoleOptions options)
{
createRole(performer, role, options);
return getResultMessageForRoleCreatedOrAltered(role, options);
}
public void grantRole(AuthenticatedUser performer, RoleResource role, RoleResource grantee) public void grantRole(AuthenticatedUser performer, RoleResource role, RoleResource grantee)
throws RequestValidationException, RequestExecutionException throws RequestValidationException, RequestExecutionException
{ {
@ -827,4 +853,52 @@ public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerM
this.invalidClientDisconnectMaxJitterMillis = duration; this.invalidClientDisconnectMaxJitterMillis = duration;
scheduleDisconnectInvalidRoleTask(); scheduleDisconnectInvalidRoleTask();
} }
private static final ColumnSpecification GENERATED_PASSWORD_METADATA = new ColumnSpecification(SchemaConstants.AUTH_KEYSPACE_NAME,
"generated_password",
new ColumnIdentifier("generated_password", true),
UTF8Type.instance);
private static final ColumnSpecification GENERATED_ROLE_NAME_METADATA = new ColumnSpecification(SchemaConstants.AUTH_KEYSPACE_NAME,
"generated_role_name",
new ColumnIdentifier("generated_role_name", true),
UTF8Type.instance);
protected ResultMessage getResultMessageForRoleCreatedOrAltered(RoleResource role, RoleOptions opts)
{
if (!opts.isGeneratedPassword() && !opts.isGeneratedName())
return null;
ResultSet resultSet = null;
if (opts.isGeneratedPassword() && !opts.isGeneratedName())
{
if (opts.getPassword().isEmpty())
return null;
resultSet = new ResultSet(new ResultSet.ResultMetadata(List.of(GENERATED_PASSWORD_METADATA)));
resultSet.addColumnValue(bytes(opts.getPassword().get()));
}
else if (!opts.isGeneratedPassword() && opts.isGeneratedName())
{
resultSet = new ResultSet(new ResultSet.ResultMetadata(List.of(GENERATED_ROLE_NAME_METADATA)));
resultSet.addColumnValue(bytes(role.getRoleName()));
}
else if (opts.isGeneratedName() && opts.isGeneratedPassword())
{
if (opts.getPassword().isEmpty())
{
resultSet = new ResultSet(new ResultSet.ResultMetadata(List.of(GENERATED_ROLE_NAME_METADATA)));
resultSet.addColumnValue(bytes(role.getRoleName()));
}
else
{
resultSet = new ResultSet(new ResultSet.ResultMetadata(List.of(GENERATED_PASSWORD_METADATA, GENERATED_ROLE_NAME_METADATA)));
resultSet.addColumnValue(bytes(opts.getPassword().get()));
resultSet.addColumnValue(bytes(role.getRoleName()));
}
}
return new ResultMessage.Rows(resultSet);
}
} }

View File

@ -27,6 +27,7 @@ import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestExecutionException; import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.transport.messages.ResultMessage;
/** /**
* Responsible for managing roles (which also includes what * Responsible for managing roles (which also includes what
@ -44,7 +45,7 @@ public interface IRoleManager extends AuthCache.BulkLoader<RoleResource, Set<Rol
*/ */
public enum Option public enum Option
{ {
SUPERUSER, PASSWORD, LOGIN, OPTIONS, HASHED_PASSWORD, GENERATED_PASSWORD SUPERUSER, PASSWORD, LOGIN, OPTIONS, HASHED_PASSWORD, GENERATED_PASSWORD, GENERATED_NAME
} }
/** /**
@ -64,7 +65,7 @@ public interface IRoleManager extends AuthCache.BulkLoader<RoleResource, Set<Rol
* options are guaranteed to be a subset of supportedOptions(). * options are guaranteed to be a subset of supportedOptions().
* *
* @param performer User issuing the create role statement. * @param performer User issuing the create role statement.
* @param role Rolei being created * @param role Role being created
* @param options Options the role will be created with * @param options Options the role will be created with
* @throws RequestValidationException * @throws RequestValidationException
* @throws RequestExecutionException * @throws RequestExecutionException
@ -72,6 +73,23 @@ public interface IRoleManager extends AuthCache.BulkLoader<RoleResource, Set<Rol
void createRole(AuthenticatedUser performer, RoleResource role, RoleOptions options) void createRole(AuthenticatedUser performer, RoleResource role, RoleOptions options)
throws RequestValidationException, RequestExecutionException; throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of a CREATE ROLE statement.
* options are guaranteed to be a subset of supportedOptions().
*
* @param performer User issuing the create role statement.
* @param role Role being created
* @param options Options the role will be created with
* @return result message to return to a caller as CQL response when a role was created. Default is {@code null}.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
default ResultMessage createRoleWithResult(AuthenticatedUser performer, RoleResource role, RoleOptions options)
{
createRole(performer, role, options);
return null;
}
/** /**
* Called during execution of DROP ROLE statement, as well we removing any main record of the role from the system * Called during execution of DROP ROLE statement, as well we removing any main record of the role from the system
* this implies that we want to revoke this role from all other roles that it has been granted to. * this implies that we want to revoke this role from all other roles that it has been granted to.
@ -98,6 +116,25 @@ public interface IRoleManager extends AuthCache.BulkLoader<RoleResource, Set<Rol
void alterRole(AuthenticatedUser performer, RoleResource role, RoleOptions options) void alterRole(AuthenticatedUser performer, RoleResource role, RoleOptions options)
throws RequestValidationException, RequestExecutionException; throws RequestValidationException, RequestExecutionException;
/**
* Called during execution of ALTER ROLE statement.
* options are always guaranteed to be a subset of supportedOptions(). Furthermore, if the actor performing the query
* is not a superuser and is altering themself, then options are guaranteed to be a subset of alterableOptions().
* Keep the body of the method blank if your implementation doesn't support modification of any options.
*
* @param performer User issuing the alter role statement.
* @param role Role that will be altered.
* @param options Options to alter.
* @return result message to return to a caller as CQL response when a role was altered. Default is {@code null}.
* @throws RequestValidationException
* @throws RequestExecutionException
*/
default ResultMessage alterRoleWithResult(AuthenticatedUser performer, RoleResource role, RoleOptions options)
{
alterRole(performer, role, options);
return null;
}
/** /**
* Called during execution of GRANT ROLE query. * Called during execution of GRANT ROLE query.
* Grant an role to another existing role. A grantee that has a role granted to it will inherit any * Grant an role to another existing role. A grantee that has a role granted to it will inherit any

View File

@ -94,6 +94,11 @@ public class RoleOptions
return (Boolean) options.getOrDefault(IRoleManager.Option.GENERATED_PASSWORD, Boolean.FALSE); return (Boolean) options.getOrDefault(IRoleManager.Option.GENERATED_PASSWORD, Boolean.FALSE);
} }
public boolean isGeneratedName()
{
return (Boolean) options.getOrDefault(IRoleManager.Option.GENERATED_NAME, Boolean.FALSE);
}
/** /**
* Return the string value of the hashed password option. * Return the string value of the hashed password option.
* @return hashed password option value * @return hashed password option value

View File

@ -54,6 +54,8 @@ public class RoleResource implements IResource, Comparable<RoleResource>
private static final String ROOT_NAME = "roles"; private static final String ROOT_NAME = "roles";
private static final RoleResource ROOT_RESOURCE = new RoleResource(); private static final RoleResource ROOT_RESOURCE = new RoleResource();
// synthetic role for saying that a role is going to be generated
public static final RoleResource GENERATED_ROLE = new RoleResource("generated");
private final Level level; private final Level level;
private final String name; private final String name;

View File

@ -1003,8 +1003,10 @@ public class Config
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_warn_threshold = null; public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_warn_threshold = null;
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_fail_threshold = null; public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_fail_threshold = null;
public volatile boolean password_validator_reconfiguration_enabled = true; public volatile boolean password_policy_reconfiguration_enabled = true;
public volatile CustomGuardrailConfig password_validator = new CustomGuardrailConfig(); public volatile boolean role_name_policy_reconfiguration_enabled = true;
public volatile CustomGuardrailConfig password_policy = new CustomGuardrailConfig();
public volatile CustomGuardrailConfig role_name_policy = new CustomGuardrailConfig();
public volatile AutoRepairConfig auto_repair = new AutoRepairConfig(); public volatile AutoRepairConfig auto_repair = new AutoRepairConfig();
/** /**

View File

@ -5923,9 +5923,14 @@ public class DatabaseDescriptor
return conf.triggers_policy; return conf.triggers_policy;
} }
public static boolean isPasswordValidatorReconfigurationEnabled() public static boolean isPasswordPolicyReconfigurationEnabled()
{ {
return conf.password_validator_reconfiguration_enabled; return conf.password_policy_reconfiguration_enabled;
}
public static boolean isRoleNamePolicyReconfigurationEnabled()
{
return conf.role_name_policy_reconfiguration_enabled;
} }
public static Config.TombstonesMetricGranularity getPurgeableTobmstonesMetricGranularity() public static Config.TombstonesMetricGranularity getPurgeableTobmstonesMetricGranularity()

View File

@ -19,6 +19,7 @@
package org.apache.cassandra.config; package org.apache.cassandra.config;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -108,7 +109,8 @@ public class GuardrailsOptions implements GuardrailsConfig
config.sai_sstable_indexes_per_query_fail_threshold, config.sai_sstable_indexes_per_query_fail_threshold,
"sai_sstable_indexes_per_query", "sai_sstable_indexes_per_query",
false); false);
validatePasswordValidator(config.password_validator); validatePasswordPolicy(config.password_policy);
validateRoleNamePolicy(config.role_name_policy);
} }
@Override @Override
@ -1038,9 +1040,15 @@ public class GuardrailsOptions implements GuardrailsConfig
} }
@Override @Override
public CustomGuardrailConfig getPasswordValidatorConfig() public CustomGuardrailConfig getPasswordPolicyConfig()
{ {
return config.password_validator; return config.password_policy;
}
@Override
public CustomGuardrailConfig getRoleNamePolicyConfig()
{
return config.role_name_policy;
} }
public void setMaximumReplicationFactorThreshold(int warn, int fail) public void setMaximumReplicationFactorThreshold(int warn, int fail)
@ -1538,8 +1546,13 @@ public class GuardrailsOptions implements GuardrailsConfig
* *
* @param config configuration to use for generator and validator * @param config configuration to use for generator and validator
*/ */
private static void validatePasswordValidator(CustomGuardrailConfig config) private static void validatePasswordPolicy(CustomGuardrailConfig config)
{ {
ValueGenerator.getGenerator("password", config).generate(ValueValidator.getValidator("password", config)); ValueGenerator.getGenerator("password_policy", config).generate(ValueValidator.getValidator("password_policy", config), Map.of());
}
private static void validateRoleNamePolicy(CustomGuardrailConfig config)
{
ValueGenerator.getGenerator("role_name_policy", config).generate(ValueValidator.getValidator("role_name_policy", config), Map.of());
} }
} }

View File

@ -17,6 +17,8 @@
*/ */
package org.apache.cassandra.cql3.statements; package org.apache.cassandra.cql3.statements;
import java.util.Map;
import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogContext;
import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.auth.AuthenticatedUser; import org.apache.cassandra.auth.AuthenticatedUser;
@ -135,19 +137,20 @@ public class AlterRoleStatement extends AuthenticationStatement
if (opts.isGeneratedPassword()) if (opts.isGeneratedPassword())
{ {
String generatedPassword = Guardrails.password.generate(); String generatedPassword = Guardrails.passwordPolicy.generate(state, Map.of());
if (generatedPassword != null) if (generatedPassword != null)
opts.setOption(IRoleManager.Option.PASSWORD, generatedPassword); opts.setOption(IRoleManager.Option.PASSWORD, generatedPassword);
else else
throw new InvalidRequestException("You have to enable password_validator and it's generator_class_name property " + throw new InvalidRequestException("You have to enable password_policy and its generator_class_name property " +
"in cassandra.yaml to be able to generate passwords."); "in cassandra.yaml to be able to generate passwords.");
} }
if (opts.getPassword().isPresent()) if (opts.getPassword().isPresent())
Guardrails.password.guard(opts.getPassword().get(), state); Guardrails.passwordPolicy.validate(opts.getPassword().get(), state);
ResultMessage resultMessage = null;
if (!opts.isEmpty()) if (!opts.isEmpty())
DatabaseDescriptor.getRoleManager().alterRole(state.getUser(), role, opts); resultMessage = DatabaseDescriptor.getRoleManager().alterRoleWithResult(state.getUser(), role, opts);
if (dcPermissions != null) if (dcPermissions != null)
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions); DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
@ -155,7 +158,7 @@ public class AlterRoleStatement extends AuthenticationStatement
if (cidrPermissions != null) if (cidrPermissions != null)
DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions); DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions);
return getResultMessage(opts); return resultMessage;
} }
@Override @Override

View File

@ -17,36 +17,20 @@
*/ */
package org.apache.cassandra.cql3.statements; package org.apache.cassandra.cql3.statements;
import java.util.List;
import org.apache.cassandra.auth.Permission; import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.auth.RoleOptions;
import org.apache.cassandra.auth.RoleResource; import org.apache.cassandra.auth.RoleResource;
import org.apache.cassandra.cql3.CQLStatement; import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.ResultSet;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.RequestExecutionException; import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestValidationException; import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState; import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.transport.messages.ResultMessage;
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
public abstract class AuthenticationStatement extends CQLStatement.Raw implements CQLStatement public abstract class AuthenticationStatement extends CQLStatement.Raw implements CQLStatement
{ {
private static final List<ColumnSpecification> GENERATED_PASSWORD_METADATA =
List.of(new ColumnSpecification(SchemaConstants.AUTH_KEYSPACE_NAME,
"generated_password",
new ColumnIdentifier("generated_password", true),
UTF8Type.instance));
public AuthenticationStatement prepare(ClientState state) public AuthenticationStatement prepare(ClientState state)
{ {
return this; return this;
@ -87,21 +71,5 @@ public abstract class AuthenticationStatement extends CQLStatement.Raw implement
{ {
return query; return query;
} }
protected ResultMessage getResultMessage(RoleOptions opts)
{
if (!opts.isGeneratedPassword())
return null;
if (opts.getPassword().isEmpty())
return null;
ResultSet.ResultMetadata resultMetadata = new ResultSet.ResultMetadata(GENERATED_PASSWORD_METADATA);
ResultSet result = new ResultSet(resultMetadata);
result.addColumnValue(bytes(opts.getPassword().get()));
return new ResultMessage.Rows(result);
}
} }

View File

@ -17,6 +17,8 @@
*/ */
package org.apache.cassandra.cql3.statements; package org.apache.cassandra.cql3.statements;
import java.util.Map;
import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogContext;
import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.audit.AuditLogEntryType;
@ -52,7 +54,11 @@ public class CreateRoleStatement extends AuthenticationStatement
public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions, public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions,
CIDRPermissions cidrPermissions, boolean ifNotExists) CIDRPermissions cidrPermissions, boolean ifNotExists)
{ {
this.role = RoleResource.role(name.getName()); if (options.isGeneratedName())
this.role = RoleResource.GENERATED_ROLE;
else
this.role = RoleResource.role(name.getName());
this.opts = options; this.opts = options;
this.dcPermissions = dcPermissions; this.dcPermissions = dcPermissions;
this.cidrPermissions = cidrPermissions; this.cidrPermissions = cidrPermissions;
@ -72,7 +78,6 @@ public class CreateRoleStatement extends AuthenticationStatement
public void validate(ClientState state) throws RequestValidationException public void validate(ClientState state) throws RequestValidationException
{ {
opts.validate(); opts.validate();
if (role.getRoleName().isEmpty()) if (role.getRoleName().isEmpty())
throw new InvalidRequestException("Role name can't be an empty string"); throw new InvalidRequestException("Role name can't be an empty string");
@ -89,49 +94,69 @@ public class CreateRoleStatement extends AuthenticationStatement
// validate login here before authorize to avoid leaking role existence to anonymous users. // validate login here before authorize to avoid leaking role existence to anonymous users.
state.ensureNotAnonymous(); state.ensureNotAnonymous();
if (!ifNotExists && DatabaseDescriptor.getRoleManager().isExistingRole(role)) if (!ifNotExists && role != RoleResource.GENERATED_ROLE && DatabaseDescriptor.getRoleManager().isExistingRole(role))
throw new InvalidRequestException(String.format("%s already exists", role.getRoleName())); throw new InvalidRequestException(String.format("%s already exists", role.getRoleName()));
} }
public ResultMessage execute(ClientState state) throws RequestExecutionException, RequestValidationException public ResultMessage execute(ClientState state) throws RequestExecutionException, RequestValidationException
{ {
// not rejected in validate() // not rejected in validate()
if (ifNotExists && DatabaseDescriptor.getRoleManager().isExistingRole(role)) if (ifNotExists && role != RoleResource.GENERATED_ROLE && DatabaseDescriptor.getRoleManager().isExistingRole(role))
return null; return null;
RoleResource roleResource;
if (opts.isGeneratedName())
{
Map<String, Object> options = (Map<String, Object>) opts.getOptions().get(IRoleManager.Option.OPTIONS);
String generatedName = Guardrails.roleNamePolicy.generate(state, options);
if (generatedName != null)
roleResource = RoleResource.role(generatedName);
else
throw new InvalidRequestException("You have to enable role_name_policy and its generator_class_name property " +
"in cassandra.yaml to be able to generate role names.");
}
else
{
roleResource = role;
}
if (opts.isGeneratedPassword()) if (opts.isGeneratedPassword())
{ {
String generatedPassword = Guardrails.password.generate(); String generatedPassword = Guardrails.passwordPolicy.generate(state);
if (generatedPassword != null) if (generatedPassword != null)
opts.setOption(IRoleManager.Option.PASSWORD, generatedPassword); opts.setOption(IRoleManager.Option.PASSWORD, generatedPassword);
else else
throw new InvalidRequestException("You have to enable password_validator and it's generator_class_name property " + throw new InvalidRequestException("You have to enable password_policy and its generator_class_name property " +
"in cassandra.yaml to be able to generate passwords."); "in cassandra.yaml to be able to generate passwords.");
} }
opts.getPassword().ifPresent(password -> Guardrails.password.guard(password, state)); opts.getPassword().ifPresent(password -> Guardrails.passwordPolicy.validate(password, state));
Guardrails.roleNamePolicy.validate(roleResource.getRoleName(), state);
ResultMessage resultMessage = DatabaseDescriptor.getRoleManager().createRoleWithResult(state.getUser(), roleResource, opts);
DatabaseDescriptor.getRoleManager().createRole(state.getUser(), role, opts);
if (DatabaseDescriptor.getNetworkAuthorizer().requireAuthorization()) if (DatabaseDescriptor.getNetworkAuthorizer().requireAuthorization())
{ {
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions); DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(roleResource, dcPermissions);
} }
if (cidrPermissions != null) if (cidrPermissions != null)
DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions); DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(roleResource, cidrPermissions);
grantPermissionsToCreator(state); grantPermissionsToCreator(roleResource, state);
return getResultMessage(opts); return resultMessage;
} }
/** /**
* Grant all applicable permissions on the newly created role to the user performing the request * Grant all applicable permissions on the newly created role to the user performing the request
* see also: AlterTableStatement#createdResources() and the overridden implementations * see also: AlterTableStatement#createdResources() and the overridden implementations
* of it in subclasses CreateKeyspaceStatement & CreateTableStatement. * of it in subclasses CreateKeyspaceStatement & CreateTableStatement.
* @param state client state *
* @param roleResource role resource to grant permissions on
* @param state client state
*/ */
private void grantPermissionsToCreator(ClientState state) private void grantPermissionsToCreator(RoleResource roleResource, ClientState state)
{ {
// The creator of a Role automatically gets ALTER/DROP/AUTHORIZE/DESCRIBE permissions on it if: // The creator of a Role automatically gets ALTER/DROP/AUTHORIZE/DESCRIBE permissions on it if:
// * the user is not anonymous // * the user is not anonymous
@ -142,8 +167,8 @@ public class CreateRoleStatement extends AuthenticationStatement
try try
{ {
DatabaseDescriptor.getAuthorizer().grant(AuthenticatedUser.SYSTEM_USER, DatabaseDescriptor.getAuthorizer().grant(AuthenticatedUser.SYSTEM_USER,
role.applicablePermissions(), roleResource.applicablePermissions(),
role, roleResource,
RoleResource.role(state.getUser().getName())); RoleResource.role(state.getUser().getName()));
} }
catch (UnsupportedOperationException e) catch (UnsupportedOperationException e)

View File

@ -18,28 +18,31 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.tracing.Tracing;
public class PasswordGuardrail extends CustomGuardrail<String> public abstract class AbstractCustomGuardrail<T> extends CustomGuardrail<T>
{ {
private static final Logger logger = LoggerFactory.getLogger(PasswordGuardrail.class);
/** /**
* @param configSupplier configuration supplier of the custom guardrail * @param name name of the custom guardrail
* @param reason guardrail reason
* @param configSupplier configuration supplier of the custom guardrail
* @param guardWhileSuperuser when true, the guardrail will be executed even the caller is a superuser. If
* false, this guardrail will be called only in case a caller is not a superuser.
*/ */
public PasswordGuardrail(Supplier<CustomGuardrailConfig> configSupplier) public AbstractCustomGuardrail(String name, String reason, Supplier<CustomGuardrailConfig> configSupplier, boolean guardWhileSuperuser)
{ {
super("password", null, configSupplier, true); super(name, reason, configSupplier, guardWhileSuperuser);
}
@Override
String decorateMessage(String message)
{
return String.format("Guardrail %s violated: %s", name, message);
} }
@Override @Override
@ -64,36 +67,8 @@ public class PasswordGuardrail extends CustomGuardrail<String>
GuardrailsDiagnostics.failed(name, redactedMsg); GuardrailsDiagnostics.failed(name, redactedMsg);
if (state != null || throwOnNullClientState) if (state != null || throwOnNullClientState)
throw new PasswordGuardrailException(message, redactedMessage); throwException(message, redactedMessage);
} }
@Override protected abstract void throwException(String message, String redactedMessage);
String decorateMessage(String message)
{
return String.format("Guardrail %s violated: %s", name, message);
}
@Override
void reconfigure(@Nullable Map<String, Object> newConfig)
{
if (!DatabaseDescriptor.isPasswordValidatorReconfigurationEnabled())
{
logger.warn("It is not possible to reconfigure password guardrail because " +
"property 'password_validator_reconfiguration_enabled' is set to false.");
return;
}
super.reconfigure(newConfig);
}
public static class PasswordGuardrailException extends GuardrailViolatedException
{
public final String redactedMessage;
PasswordGuardrailException(String message, String redactedMessage)
{
super(message);
this.redactedMessage = redactedMessage;
}
}
} }

View File

@ -28,6 +28,8 @@ import static java.lang.String.format;
public class CassandraPasswordConfiguration public class CassandraPasswordConfiguration
{ {
public static final int MAX_LENGTH = 1000;
public static final int MAX_CHARACTERISTICS = 4; public static final int MAX_CHARACTERISTICS = 4;
// default values // default values
@ -173,6 +175,11 @@ public class CassandraPasswordConfiguration
if (lengthWarn < 0) throw mustBePositiveException(LENGTH_WARN_KEY); if (lengthWarn < 0) throw mustBePositiveException(LENGTH_WARN_KEY);
if (lengthFail < 0) throw mustBePositiveException(LENGTH_FAIL_KEY); if (lengthFail < 0) throw mustBePositiveException(LENGTH_FAIL_KEY);
if (MAX_LENGTH < maxLength)
throw new ConfigurationException(format("%s can not be greater than %s",
MAX_LENGTH_KEY,
MAX_LENGTH));
if (maxLength <= lengthWarn) if (maxLength <= lengthWarn)
throw getValidationException(MAX_LENGTH_KEY, maxLength, LENGTH_WARN_KEY, lengthWarn); throw getValidationException(MAX_LENGTH_KEY, maxLength, LENGTH_WARN_KEY, lengthWarn);

View File

@ -20,6 +20,7 @@ package org.apache.cassandra.db.guardrails;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
@ -54,16 +55,13 @@ public class CassandraPasswordGenerator extends ValueGenerator<String>
} }
@Override @Override
public String generate(int size, ValueValidator<String> validator) public String generate(ValueValidator<String> validator, Map<String, Object> options)
{ {
if (size > configuration.maxLength)
throw new ConfigurationException("Unable to generate a password of length " + size);
boolean dictionaryAware = validator instanceof PasswordDictionaryAware; boolean dictionaryAware = validator instanceof PasswordDictionaryAware;
for (int i = 0; i < maxPasswordGenerationAttempts; i++) for (int i = 0; i < maxPasswordGenerationAttempts; i++)
{ {
String generatedPassword = passwordGenerator.generatePassword(size, characterRules); String generatedPassword = passwordGenerator.generatePassword(configuration.lengthWarn, characterRules);
if (validator.shouldWarn(generatedPassword, false).isEmpty()) if (validator.shouldWarn(generatedPassword, false).isEmpty())
{ {
if (!dictionaryAware || ((PasswordDictionaryAware<?>) validator).foundInDictionary(generatedPassword).isValid()) if (!dictionaryAware || ((PasswordDictionaryAware<?>) validator).foundInDictionary(generatedPassword).isValid())
@ -76,12 +74,6 @@ public class CassandraPasswordGenerator extends ValueGenerator<String>
"Check your configuration and try again."); "Check your configuration and try again.");
} }
@Override
public String generate(ValueValidator<String> validator)
{
return generate(configuration.lengthWarn, validator);
}
@Nonnull @Nonnull
@Override @Override
public CustomGuardrailConfig getParameters() public CustomGuardrailConfig getParameters()

View File

@ -47,6 +47,7 @@ public class CustomGuardrail<VALUE> extends Guardrail
/** /**
* @param name name of the custom guardrail * @param name name of the custom guardrail
* @param reason guardrail reason
* @param configSupplier configuration supplier of the custom guardrail * @param configSupplier configuration supplier of the custom guardrail
* @param guardWhileSuperuser when true, the guardrail will be executed even the caller is a superuser. If * @param guardWhileSuperuser when true, the guardrail will be executed even the caller is a superuser. If
* false, this guardrail will be called only in case a caller is not a superuser. * false, this guardrail will be called only in case a caller is not a superuser.
@ -84,7 +85,7 @@ public class CustomGuardrail<VALUE> extends Guardrail
* @param value value to validate by the validator of this guardrail * @param value value to validate by the validator of this guardrail
* @param state client's state * @param state client's state
*/ */
public void guard(VALUE value, ClientState state) public void validate(VALUE value, ClientState state)
{ {
if (!enabled(state)) if (!enabled(state))
return; return;
@ -111,28 +112,37 @@ public class CustomGuardrail<VALUE> extends Guardrail
*/ */
public CustomGuardrailConfig getConfig() public CustomGuardrailConfig getConfig()
{ {
return getValidator().getParameters(); CustomGuardrailConfig config = new CustomGuardrailConfig(getValidator().getParameters());
config.put(ValueValidator.VALIDATOR_CLASS_NAME_KEY, getValidator().getClass().getName());
config.put(ValueGenerator.GENERATOR_CLASS_NAME_KEY, getGenerator().getClass().getName());
return config;
} }
/** public VALUE generate(ClientState state)
* Generates a value of given size.
*
* @param size size of value to be generated
* @return generated value of given size
*/
public VALUE generate(int size)
{ {
return getGenerator().generate(size, getValidator()); return generate(state, Map.of());
} }
/** /**
* Generates a valid value. * Generates a valid value.
* *
* @param state client state
* @param options options to use upon generation
* @return generated and valid value * @return generated and valid value
*/ */
public VALUE generate() public VALUE generate(ClientState state, Map<String, Object> options)
{ {
return getGenerator().generate(getValidator()); try
{
return getGenerator().generate(getValidator(), options);
}
catch (Throwable t)
{
fail(t.getMessage(),
t.getMessage(),
state);
return null;
}
} }
/** /**
@ -149,6 +159,7 @@ public class CustomGuardrail<VALUE> extends Guardrail
*/ */
void reconfigure(@Nullable Map<String, Object> newConfig) void reconfigure(@Nullable Map<String, Object> newConfig)
{ {
Map<String, Object> mergedMap = new HashMap<>(getValidator().getParameters()); Map<String, Object> mergedMap = new HashMap<>(getValidator().getParameters());
if (newConfig != null) if (newConfig != null)

View File

@ -19,7 +19,6 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -29,6 +28,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DataStorageSpec; import org.apache.cassandra.config.DataStorageSpec;
import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DatabaseDescriptor;
@ -39,6 +39,7 @@ import org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy;
import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.disk.usage.DiskUsageBroadcaster; import org.apache.cassandra.service.disk.usage.DiskUsageBroadcaster;
import org.apache.cassandra.utils.JsonUtils;
import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.MBeanWrapper;
import static java.lang.String.format; import static java.lang.String.format;
@ -556,8 +557,14 @@ public final class Guardrails implements GuardrailsMBean
/** /**
* Guardrail on passwords for CREATE / ALTER ROLE statements. * Guardrail on passwords for CREATE / ALTER ROLE statements.
*/ */
public static final PasswordGuardrail password = public static final PasswordPolicyGuardrail passwordPolicy =
new PasswordGuardrail(() -> CONFIG_PROVIDER.getOrCreate(null).getPasswordValidatorConfig()); new PasswordPolicyGuardrail(() -> CONFIG_PROVIDER.getOrCreate(null).getPasswordPolicyConfig());
/**
* Guardrail on name for CREATE ROLE / USER statements.
*/
public static final RoleNamePolicyGuardrail roleNamePolicy =
new RoleNamePolicyGuardrail(() -> CONFIG_PROVIDER.getOrCreate(null).getRoleNamePolicyConfig());
static static
{ {
@ -1506,15 +1513,55 @@ public final class Guardrails implements GuardrailsMBean
} }
@Override @Override
public Map<String, Object> getPasswordValidatorConfig() public String getPasswordPolicy()
{ {
return password.getConfig(); try
{
return JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(passwordPolicy.getConfig());
}
catch (Throwable t)
{
throw new RuntimeException("Unable to serialize password_policy configuration");
}
} }
@Override @Override
public void reconfigurePasswordValidator(Map<String, Object> config) public String getRoleNamePolicy()
{ {
password.reconfigure(config); try
{
return JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(roleNamePolicy.getConfig());
}
catch (Throwable t)
{
throw new RuntimeException("Unable to serialize role_name_policy configuration");
}
}
@Override
public void setPasswordPolicy(String value)
{
try
{
passwordPolicy.reconfigure(JsonUtils.JSON_OBJECT_MAPPER.readValue(value, new TypeReference<>() {}));
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}
@Override
public void setRoleNamePolicy(String value)
{
try
{
roleNamePolicy.reconfigure(JsonUtils.JSON_OBJECT_MAPPER.readValue(value, new TypeReference<>() {}));
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
} }
@Override @Override

View File

@ -644,7 +644,12 @@ public interface GuardrailsConfig
void setNonPartitionRestrictedQueryEnabled(boolean enabled); void setNonPartitionRestrictedQueryEnabled(boolean enabled);
/** /**
* @return configuration for password validation guardrail. * @return configuration for password policy guardrail.
*/ */
CustomGuardrailConfig getPasswordValidatorConfig(); CustomGuardrailConfig getPasswordPolicyConfig();
/**
* @return configuration for role name policy guardrail.
*/
CustomGuardrailConfig getRoleNamePolicyConfig();
} }

View File

@ -18,7 +18,6 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -1127,14 +1126,26 @@ public interface GuardrailsMBean
void setIntersectFilteringQueryEnabled(boolean value); void setIntersectFilteringQueryEnabled(boolean value);
/** /**
* @return the configuration of password validator. * @return JSON representation of the configuration of password policy
*/ */
Map<String, Object> getPasswordValidatorConfig(); String getPasswordPolicy();
/**
* @return JSON representation of the configuration of role name policy
*/
String getRoleNamePolicy();
/** /**
* Reconfigures password validator. * Reconfigures password validator.
* *
* @param config configuration of new password validator * @param value configuration of new password validator
*/ */
void reconfigurePasswordValidator(Map<String, Object> config); void setPasswordPolicy(String value);
/**
* Reconfigures role name validator.
*
* @param value configuration of new role name validator.
*/
void setRoleNamePolicy(String value);
} }

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Map;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
@ -28,9 +29,9 @@ import org.apache.cassandra.exceptions.ConfigurationException;
*/ */
public class NoOpGenerator<VALUE> extends ValueGenerator<VALUE> public class NoOpGenerator<VALUE> extends ValueGenerator<VALUE>
{ {
private static final CustomGuardrailConfig config = new CustomGuardrailConfig(); private static final CustomGuardrailConfig config = new CustomGuardrailConfig(Map.of(GENERATOR_CLASS_NAME_KEY, NoOpGenerator.class.getCanonicalName()));
public static NoOpGenerator INSTANCE = new NoOpGenerator<>(config); public static final NoOpGenerator INSTANCE = new NoOpGenerator<>(config);
public NoOpGenerator(CustomGuardrailConfig unused) public NoOpGenerator(CustomGuardrailConfig unused)
{ {
@ -38,13 +39,7 @@ public class NoOpGenerator<VALUE> extends ValueGenerator<VALUE>
} }
@Override @Override
public VALUE generate(int size, ValueValidator<VALUE> validator) public VALUE generate(ValueValidator<VALUE> validator, Map<String, Object> options)
{
return null;
}
@Override
public VALUE generate(ValueValidator<VALUE> validator)
{ {
return null; return null;
} }

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -30,7 +31,9 @@ import org.apache.cassandra.exceptions.ConfigurationException;
*/ */
public class NoOpValidator<T> extends ValueValidator<T> public class NoOpValidator<T> extends ValueValidator<T>
{ {
private static final CustomGuardrailConfig config = new CustomGuardrailConfig(); private static final CustomGuardrailConfig config = new CustomGuardrailConfig(Map.of(VALIDATOR_CLASS_NAME_KEY, NoOpValidator.class.getCanonicalName()));
public static final NoOpValidator INSTANCE = new NoOpValidator<>(config);
public NoOpValidator(CustomGuardrailConfig unused) public NoOpValidator(CustomGuardrailConfig unused)
{ {

View File

@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
public class PasswordPolicyGuardrail extends AbstractCustomGuardrail<String>
{
private static final Logger logger = LoggerFactory.getLogger(PasswordPolicyGuardrail.class);
/**
* @param configSupplier configuration supplier of the custom guardrail
*/
public PasswordPolicyGuardrail(Supplier<CustomGuardrailConfig> configSupplier)
{
super("password_policy", null, configSupplier, true);
}
@Override
void reconfigure(@Nullable Map<String, Object> newConfig)
{
if (!DatabaseDescriptor.isPasswordPolicyReconfigurationEnabled())
{
logger.warn("It is not possible to reconfigure password_policy guardrail because " +
"property 'password_policy_reconfiguration_enabled' is set to false.");
return;
}
super.reconfigure(newConfig);
}
@Override
protected void throwException(String message, String redactedMessage)
{
throw new PasswordGuardrailException(message, redactedMessage);
}
public static class PasswordGuardrailException extends GuardrailViolatedException
{
public final String redactedMessage;
PasswordGuardrailException(String message, String redactedMessage)
{
super(message);
this.redactedMessage = redactedMessage;
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
public class RoleNamePolicyGuardrail extends AbstractCustomGuardrail<String>
{
private static final Logger logger = LoggerFactory.getLogger(RoleNamePolicyGuardrail.class);
/**
* @param configSupplier configuration supplier of the custom guardrail
*/
public RoleNamePolicyGuardrail(Supplier<CustomGuardrailConfig> configSupplier)
{
super("role_name_policy", null, configSupplier, true);
}
@Override
void reconfigure(@Nullable Map<String, Object> newConfig)
{
if (!DatabaseDescriptor.isRoleNamePolicyReconfigurationEnabled())
{
logger.warn("It is not possible to reconfigure role_name_policy guardrail because " +
"property 'role_name_policy_reconfiguration_enabled' is set to false.");
return;
}
super.reconfigure(newConfig);
}
@Override
protected void throwException(String message, String redactedMessage)
{
throw new RoleNamePolicyGuardrailException(message, redactedMessage);
}
public static class RoleNamePolicyGuardrailException extends GuardrailViolatedException
{
public final String redactedMessage;
RoleNamePolicyGuardrailException(String message, String redactedMessage)
{
super(message);
this.redactedMessage = redactedMessage;
}
}
}

View File

@ -0,0 +1,192 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import org.apache.cassandra.exceptions.ConfigurationException;
/**
* Generates random name for user / role.
* Generator reacts on these parameters when specified in OPTIONS map in CQL:
* <ul>
* <li>name_prefix - arbitrary string to prepend to generated name</li>
* <li>name_suffix - arbitraty string to append to generated name</li>
* <li>name_size - size of generated string. Can not be less than 10 and more than 32</li>
* </ul>
* <p>
* Additionally, it reacts to this parameter put in cassandra.yaml:
*
* <ul>
* <li>min_generated_name_size - sets minimum size of name. If {@code name_size} is specified in CQL's OPTIONS,
* it will be checked against this minimum size and fail if smaller than that.
* </li>
* </ul>
* <p>
* {@code name_prefix} and {@code name_suffix} string lengths are not included in {@code name_size} length.
* {@code name_size} is only related to generated part of the role name.
* <p>
* The first character of generated role name will be always one of {@code a,b,c,d,e,f}.
*/
public class UUIDRoleNameGenerator extends ValueGenerator<String>
{
public static final String NAME_PREFIX_KEY = "name_prefix";
public static final String NAME_SUFFIX_KEY = "name_suffix";
public static final String NAME_SIZE = "name_size";
public static final String MINIMUM_NAME_SIZE_CONFIG_OPTION = "min_generated_name_size";
public static final int DEFAULT_MINIMUM_NAME_SIZE = 10;
private static final Set<String> KEYS_IN_OPTIONS = Set.of(NAME_PREFIX_KEY, NAME_SUFFIX_KEY, NAME_SIZE);
private static final Pattern PATTERN = Pattern.compile("-");
public static final char[] FIRST_CHARS = { 'a', 'b', 'c', 'd', 'e', 'f' };
private static final Random random = new Random();
// lenght of UUID without hyphens
// generated name can be longer than this if it has prefix / suffix
public static final int MAXIMUM_NAME_SIZE = 32;
private final int minimumNameSize;
public UUIDRoleNameGenerator()
{
this(new CustomGuardrailConfig());
}
public UUIDRoleNameGenerator(CustomGuardrailConfig config)
{
super(config);
minimumNameSize = config.resolveInteger(MINIMUM_NAME_SIZE_CONFIG_OPTION, DEFAULT_MINIMUM_NAME_SIZE);
validateParameters();
}
@Override
public Set<String> getSupportedOptionsKeys()
{
return KEYS_IN_OPTIONS;
}
@Override
public String generate(ValueValidator<String> validator, Map<String, Object> options)
{
int size = getSize(options);
// to always start on a letter, so we do not need to wrap in ''
char firstChar = FIRST_CHARS[random.nextInt(6)];
String uuid = UUID.randomUUID().toString();
String uuidWithoutHyphens = PATTERN.matcher(uuid).replaceAll("");
String name = firstChar + uuidWithoutHyphens.substring(1);
name = name.substring(0, size);
name = enrich(NAME_PREFIX_KEY, name, options);
name = enrich(NAME_SUFFIX_KEY, name, options);
return name;
}
@Nonnull
@Override
public CustomGuardrailConfig getParameters()
{
return config;
}
@Override
public void validateParameters() throws ConfigurationException
{
if (minimumNameSize < DEFAULT_MINIMUM_NAME_SIZE || minimumNameSize > MAXIMUM_NAME_SIZE)
throw new ConfigurationException(MINIMUM_NAME_SIZE_CONFIG_OPTION + " has to be at least " + DEFAULT_MINIMUM_NAME_SIZE + " and at most " + MAXIMUM_NAME_SIZE);
}
private String enrich(String key, String generatedValue, Map<String, Object> options)
{
if (options == null || options.isEmpty())
return generatedValue;
if (options.containsKey(key))
{
Object value = options.get(key);
if (value == null)
throw new IllegalArgumentException("Value of " + key + " cannot be null.");
if (!(value instanceof String))
throw new IllegalArgumentException("Value of " + key + " is not a string.");
if (NAME_PREFIX_KEY.equals(key))
generatedValue = value + generatedValue;
else if (NAME_SUFFIX_KEY.equals(key))
generatedValue = generatedValue + value;
}
return generatedValue;
}
private int getSize(Map<String, Object> options)
{
Object sizeObject;
if (options == null)
{
sizeObject = MAXIMUM_NAME_SIZE;
}
else if (options.containsKey(NAME_SIZE))
{
Object nameSizeValue = options.get(NAME_SIZE);
if (nameSizeValue != null)
sizeObject = nameSizeValue;
else
throw new IllegalArgumentException("Value of " + NAME_SIZE + " has to be strictly positive integer.");
}
else
{
sizeObject = MAXIMUM_NAME_SIZE;
}
int size;
if (sizeObject instanceof String)
{
try
{
size = Integer.parseInt((String) sizeObject);
}
catch (Throwable t)
{
throw new IllegalArgumentException("Value '" + sizeObject + "' can't be converted to integer.");
}
}
else if (sizeObject instanceof Number)
size = ((Number) sizeObject).intValue();
else
throw new IllegalArgumentException("Unsupported object passed to resolve " + NAME_SIZE + ": " + sizeObject.getClass().getName());
if (size < minimumNameSize)
throw new IllegalArgumentException("Value of " + NAME_SIZE + " parameter has to be at least " + minimumNameSize + '.');
if (size > MAXIMUM_NAME_SIZE)
throw new IllegalArgumentException("Generator generates names of maximum length " + MAXIMUM_NAME_SIZE + ". " +
"You want to generate with length " + size + '.');
return size;
}
}

View File

@ -18,6 +18,8 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -27,20 +29,16 @@ import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.FBUtilities;
import static java.lang.String.format; import static java.lang.String.format;
import static java.util.Map.of;
/** /**
* Generates a value which respective {@link ValueValidator} successfuly validates. * Generates a value which respective {@link ValueValidator} successfuly validates.
*/ */
public abstract class ValueGenerator<VALUE> public abstract class ValueGenerator<VALUE>
{ {
private static final Logger logger = LoggerFactory.getLogger(ValueValidator.class); private static final Logger logger = LoggerFactory.getLogger(ValueGenerator.class);
public static final String GENERATOR_CLASS_NAME_KEY = "generator_class_name"; public static final String GENERATOR_CLASS_NAME_KEY = "generator_class_name";
private static final ValueGenerator<?> NO_OP_GENERATOR =
new NoOpGenerator<>(new CustomGuardrailConfig(of(GENERATOR_CLASS_NAME_KEY, NoOpGenerator.class.getCanonicalName())));
private static final String DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE = ValueGenerator.class.getPackage().getName(); private static final String DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE = ValueGenerator.class.getPackage().getName();
protected final CustomGuardrailConfig config; protected final CustomGuardrailConfig config;
@ -51,21 +49,46 @@ public abstract class ValueGenerator<VALUE>
} }
/** /**
* Generates a value of given size. * Generates a value. Generated value will not be validated before returning in any way.
* *
* @param size size of value to be generated * @return generated value.
* @param validator validator to validate generated value with
* @return generated value of given size
*/ */
public abstract VALUE generate(int size, ValueValidator<VALUE> validator); public VALUE generate()
{
return generate((NoOpValidator<VALUE>) NoOpValidator.INSTANCE);
}
/** /**
* Generates a valid value. * Generate a value.
*
* @param validator validator possible to use when value is internally generated, to be sure that
* what was generated is valid.
* @return generated value
*/
public VALUE generate(ValueValidator<VALUE> validator)
{
return generate(validator, Map.of());
}
/**
* Generate a value with given runtime options from OPTIONS map.
* Generated value will not be validated before returning in any way.
*
* @return generated value
*/
public VALUE generate(Map<String, Object> options)
{
return generate((NoOpValidator<VALUE>) NoOpValidator.INSTANCE, options);
}
/**
* Generate a value.
* *
* @param validator validator to validate generated value with * @param validator validator to validate generated value with
* @return generated and valid value * @param options runtime options to be used upon generation, from CQL OPTIONS, if any.
* @return generated value of given size
*/ */
public abstract VALUE generate(ValueValidator<VALUE> validator); public abstract VALUE generate(ValueValidator<VALUE> validator, Map<String, Object> options);
/** /**
* @return parameters for this generator * @return parameters for this generator
@ -80,6 +103,17 @@ public abstract class ValueGenerator<VALUE>
*/ */
public abstract void validateParameters() throws ConfigurationException; public abstract void validateParameters() throws ConfigurationException;
/**
* Tells what keys this generator supports when they are specified in CQL in OPTIONS.
* The default implementation returns empty set.
*
* @return set of keys for OPTIONS in CQL
*/
public Set<String> getSupportedOptionsKeys()
{
return Set.of();
}
/** /**
* Returns an instance of a validator according to the parameters in {@code config}. * Returns an instance of a validator according to the parameters in {@code config}.
* *
@ -100,7 +134,7 @@ public abstract class ValueGenerator<VALUE>
logger.debug("Configuration for generator for guardrail '{}' does not contain key " + logger.debug("Configuration for generator for guardrail '{}' does not contain key " +
"'generator_class_name' or its value is null or empty string. No-op generator will be used.", "'generator_class_name' or its value is null or empty string. No-op generator will be used.",
name); name);
return (ValueGenerator<VALUE>) NO_OP_GENERATOR; return (ValueGenerator<VALUE>) NoOpGenerator.INSTANCE;
} }
if (!className.contains(".")) if (!className.contains("."))

View File

@ -28,7 +28,6 @@ import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.FBUtilities;
import static java.lang.String.format; import static java.lang.String.format;
import static java.util.Map.of;
/** /**
* Validates a value by calling {@link ValueValidator#shouldFail} or {@link ValueValidator#shouldWarn} methods. * Validates a value by calling {@link ValueValidator#shouldFail} or {@link ValueValidator#shouldWarn} methods.
@ -41,10 +40,7 @@ public abstract class ValueValidator<VALUE>
{ {
private static final Logger logger = LoggerFactory.getLogger(ValueValidator.class); private static final Logger logger = LoggerFactory.getLogger(ValueValidator.class);
public static final String CLASS_NAME_KEY = "class_name"; public static final String VALIDATOR_CLASS_NAME_KEY = "validator_class_name";
private static final ValueValidator<?> NO_OP_VALIDATOR =
new NoOpValidator<>(new CustomGuardrailConfig(of(CLASS_NAME_KEY, NoOpValidator.class.getCanonicalName())));
private static final String DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE = ValueValidator.class.getPackage().getName(); private static final String DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE = ValueValidator.class.getPackage().getName();
@ -76,7 +72,7 @@ public abstract class ValueValidator<VALUE>
* Test a value to see if it emits warnings. * Test a value to see if it emits warnings.
* *
* @param value value to validate * @param value value to validate
* @param calledBySuperuser client state * @param calledBySuperuser whether this is called by a super-user or not
* @return if optional is empty, value is valid, otherwise it returns warning violation message * @return if optional is empty, value is valid, otherwise it returns warning violation message
*/ */
public abstract Optional<ValidationViolation> shouldWarn(VALUE value, boolean calledBySuperuser); public abstract Optional<ValidationViolation> shouldWarn(VALUE value, boolean calledBySuperuser);
@ -116,13 +112,13 @@ public abstract class ValueValidator<VALUE>
*/ */
public static <VALUE> ValueValidator<VALUE> getValidator(String name, @Nonnull CustomGuardrailConfig config) public static <VALUE> ValueValidator<VALUE> getValidator(String name, @Nonnull CustomGuardrailConfig config)
{ {
String className = config.resolveString(CLASS_NAME_KEY); String className = config.resolveString(VALIDATOR_CLASS_NAME_KEY);
if (className == null || className.isEmpty()) if (className == null || className.isEmpty())
{ {
logger.debug("Configuration for validator for guardrail '{}' does not contain key " + logger.debug("Configuration for validator for guardrail '{}' does not contain key " +
"'class_name' or its value is null or empty string. No-op validator will be used.", name); "'validator_class_name' or its value is null or empty string. No-op validator will be used.", name);
return (ValueValidator<VALUE>) NO_OP_VALIDATOR; return (ValueValidator<VALUE>) NoOpValidator.INSTANCE;
} }
if (!className.contains(".")) if (!className.contains("."))

View File

@ -365,7 +365,7 @@ public abstract class GuardrailsConfigCommand extends AbstractCommand
"SimpleStrategyEnabled", "simplestrategy_enabled", "SimpleStrategyEnabled", "simplestrategy_enabled",
"NonPartitionRestrictedQueryEnabled", "non_partition_restricted_index_query_enabled"); "NonPartitionRestrictedQueryEnabled", "non_partition_restricted_index_query_enabled");
private static final Set<String> ignored = Set.of("password_validator_config"); private static final Set<String> ignored = Set.of("password_policy", "role_name_policy");
/** /**
* Set of guardrails which are flags, even though their suffix would suggest they are part of "values" which have warned, ignored, and disallowed sub-categories * Set of guardrails which are flags, even though their suffix would suggest they are part of "values" which have warned, ignored, and disallowed sub-categories

View File

@ -0,0 +1,165 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.test.guardrails;
import java.util.Map;
import javax.management.Attribute;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import org.junit.Test;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.cassandra.db.guardrails.CassandraPasswordGenerator;
import org.apache.cassandra.db.guardrails.CassandraPasswordValidator;
import org.apache.cassandra.db.guardrails.CustomGuardrailConfig;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
import org.apache.cassandra.distributed.shared.JMXUtil;
import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.exceptions.ConfigurationException;
import static org.apache.cassandra.db.guardrails.Guardrails.MBEAN_NAME;
import static org.apache.cassandra.distributed.Cluster.build;
import static org.apache.cassandra.distributed.api.Feature.JMX;
import static org.apache.cassandra.utils.JsonUtils.JSON_OBJECT_MAPPER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class GuardrailPasswordPolicyTest extends TestBaseImpl
{
@Test
public void testInvalidConfigurationPreventsNodeFromStart()
{
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put("validator_class_name", CassandraPasswordValidator.class.getName());
config.put("generator_class_name", CassandraPasswordGenerator.class.getName());
config.put("illegal_sequence_length", -1);
try (Cluster ignored = build(1)
.withConfig(c -> c.set("password_policy", config))
.start())
{
fail("should throw ConfigurationException");
}
catch (Exception ex)
{
assertEquals(ConfigurationException.class.getName(), ex.getClass().getName());
}
}
@Test
public void testDisabledReconfiguration() throws Throwable
{
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put("validator_class_name", CassandraPasswordValidator.class.getName());
config.put("generator_class_name", CassandraPasswordGenerator.class.getName());
try (Cluster cluster = build(1)
.withConfig(c -> c.with(JMX)
.set("password_policy_reconfiguration_enabled", false)
.set("password_policy", config))
.start();
JMXConnector connector = JMXUtil.getJmxConnector(cluster.get(1).config()))
{
MBeanServerConnection mbsc = connector.getMBeanServerConnection();
config.put("illegal_sequence_length", -1);
mbsc.setAttribute(ObjectName.getInstance(MBEAN_NAME), new Attribute("PasswordPolicy", JSON_OBJECT_MAPPER.writeValueAsString(config)));
assertFalse(cluster.get(1)
.logs()
.watchFor("It is not possible to reconfigure password_policy guardrail because " +
"property 'password_policy_reconfiguration_enabled' is set to false.")
.getResult()
.isEmpty());
}
}
@Test
public void testEnabledReconfiguration() throws Throwable
{
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put("validator_class_name", CassandraPasswordValidator.class.getName());
config.put("generator_class_name", CassandraPasswordGenerator.class.getName());
try (Cluster cluster = build(1)
.withConfig(c -> c.with(JMX)
.set("password_policy_reconfiguration_enabled", true)
.set("password_policy", config))
.start();
JMXConnector connector = JMXUtil.getJmxConnector(cluster.get(1).config()))
{
MBeanServerConnection mbsc = connector.getMBeanServerConnection();
config.put("illegal_sequence_length", 2);
try
{
mbsc.setAttribute(ObjectName.getInstance(MBEAN_NAME), new Attribute("PasswordPolicy", JSON_OBJECT_MAPPER.writeValueAsString(config)));
fail();
}
catch (Throwable t)
{
assertTrue(t.getMessage() != null &&
t.getMessage().contains("Unable to create instance of validator of class " +
CassandraPasswordValidator.class.getName() + ": " +
"Illegal sequence length can not be lower than 3."));
}
config.put("illegal_sequence_length", 4);
mbsc.setAttribute(ObjectName.getInstance(MBEAN_NAME), new Attribute("PasswordPolicy", JSON_OBJECT_MAPPER.writeValueAsString(config)));
assertFalse(cluster.get(1)
.logs()
.watchFor("illegal_sequence_length=4")
.getResult()
.isEmpty());
Integer illegalSequenceLength = cluster.get(1).callsOnInstance((IIsolatedExecutor.SerializableCallable<Integer>)
() -> {
String passwordPolicyConfig = Guardrails.instance.getPasswordPolicy();
try
{
return JSON_OBJECT_MAPPER.readTree(passwordPolicyConfig).get("illegal_sequence_length").asInt();
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}).call();
assertEquals(4, illegalSequenceLength.intValue());
String retrievedConfig = (String) mbsc.getAttribute(ObjectName.getInstance(MBEAN_NAME), "PasswordPolicy");
Map<String, Object> configMap = JSON_OBJECT_MAPPER.readValue(retrievedConfig, new TypeReference<>() {});
mbsc.setAttribute(ObjectName.getInstance(MBEAN_NAME), new Attribute("PasswordPolicy", JSON_OBJECT_MAPPER.writeValueAsString(configMap)));
assertEquals(4,
JSON_OBJECT_MAPPER.readTree((String) mbsc.getAttribute(ObjectName.getInstance(MBEAN_NAME), "PasswordPolicy"))
.get("illegal_sequence_length").asInt());
}
}
}

View File

@ -18,41 +18,39 @@
package org.apache.cassandra.distributed.test.guardrails; package org.apache.cassandra.distributed.test.guardrails;
import java.util.Map; import javax.management.Attribute;
import javax.management.MBeanServerConnection; import javax.management.MBeanServerConnection;
import javax.management.ObjectName; import javax.management.ObjectName;
import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnector;
import org.junit.Test; import org.junit.Test;
import org.apache.cassandra.db.guardrails.CassandraPasswordGenerator;
import org.apache.cassandra.db.guardrails.CassandraPasswordValidator;
import org.apache.cassandra.db.guardrails.CustomGuardrailConfig; import org.apache.cassandra.db.guardrails.CustomGuardrailConfig;
import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.guardrails.UUIDRoleNameGenerator;
import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.shared.JMXUtil; import org.apache.cassandra.distributed.shared.JMXUtil;
import org.apache.cassandra.distributed.test.TestBaseImpl; import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import static org.apache.cassandra.db.guardrails.Guardrails.MBEAN_NAME;
import static org.apache.cassandra.distributed.Cluster.build; import static org.apache.cassandra.distributed.Cluster.build;
import static org.apache.cassandra.distributed.api.Feature.JMX;
import static org.apache.cassandra.utils.JsonUtils.JSON_OBJECT_MAPPER;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
public class GuardrailPasswordTest extends TestBaseImpl public class GuardrailsRoleNamePolicyTest extends TestBaseImpl
{ {
@Test @Test
public void testInvalidConfigurationPreventsNodeFromStart() public void testInvalidConfigurationPreventsNodeFromStart()
{ {
CustomGuardrailConfig config = new CustomGuardrailConfig(); CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put("class_name", CassandraPasswordValidator.class.getName()); config.put("generator_class_name", UUIDRoleNameGenerator.class.getName());
config.put("generator_class_name", CassandraPasswordGenerator.class.getName()); config.put("min_generated_name_size", 5);
config.put("illegal_sequence_length", -1);
try (Cluster ignored = build(1) try (Cluster ignored = build(1)
.withConfig(c -> c.with(Feature.NETWORK, Feature.NATIVE_PROTOCOL, Feature.GOSSIP) .withConfig(c -> c.set("role_name_policy", config))
.set("password_validator", config))
.start()) .start())
{ {
fail("should throw ConfigurationException"); fail("should throw ConfigurationException");
@ -67,29 +65,23 @@ public class GuardrailPasswordTest extends TestBaseImpl
public void testDisabledReconfiguration() throws Throwable public void testDisabledReconfiguration() throws Throwable
{ {
CustomGuardrailConfig config = new CustomGuardrailConfig(); CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put("class_name", CassandraPasswordValidator.class.getName()); config.put("generator_class_name", UUIDRoleNameGenerator.class.getName());
config.put("generator_class_name", CassandraPasswordGenerator.class.getName());
try (Cluster cluster = build(1) try (Cluster cluster = build(1)
.withConfig(c -> c.with(Feature.JMX) .withConfig(c -> c.with(JMX)
.set("password_validator_reconfiguration_enabled", false) .set("role_name_policy_reconfiguration_enabled", false)
.set("password_validator", config)) .set("role_name_policy", config))
.start(); .start();
JMXConnector connector = JMXUtil.getJmxConnector(cluster.get(1).config())) JMXConnector connector = JMXUtil.getJmxConnector(cluster.get(1).config()))
{ {
MBeanServerConnection mbsc = connector.getMBeanServerConnection(); MBeanServerConnection mbsc = connector.getMBeanServerConnection();
config.put("illegal_sequence_length", -1); mbsc.setAttribute(ObjectName.getInstance(MBEAN_NAME), new Attribute("RoleNamePolicy", JSON_OBJECT_MAPPER.writeValueAsString(config)));
mbsc.invoke(ObjectName.getInstance(Guardrails.MBEAN_NAME),
"reconfigurePasswordValidator",
new Object[]{ config },
new String[]{ Map.class.getName() });
assertFalse(cluster.get(1) assertFalse(cluster.get(1)
.logs() .logs()
.watchFor("It is not possible to reconfigure password guardrail because " + .watchFor("It is not possible to reconfigure role_name_policy guardrail because " +
"property 'password_validator_reconfiguration_enabled' is set to false.") "property 'role_name_policy_reconfiguration_enabled' is set to false.")
.getResult() .getResult()
.isEmpty()); .isEmpty());
} }

View File

@ -0,0 +1,122 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Ignore;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.transport.Message;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.JsonUtils;
import org.apache.cassandra.utils.Pair;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@Ignore
public abstract class AbstractGenerationalTest extends GuardrailTester
{
@After
public void afterTest() throws Throwable
{
setRoleNamePolicy(Map.of());
setPasswordPolicy(Map.of());
}
protected void setRoleNamePolicy(Map<String, Object> config)
{
try
{
guardrails().setRoleNamePolicy(JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(config));
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}
protected void setPasswordPolicy(Map<String, Object> config)
{
try
{
guardrails().setPasswordPolicy(JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(config));
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}
protected String extractGeneratedPassword(ResultMessage resultMessage)
{
return extractOne(resultMessage, "generated_password");
}
protected String extractGeneratedRoleName(ResultMessage resultMessage)
{
return extractOne(resultMessage, "generated_role_name");
}
private String extractOne(ResultMessage resultMessage, String columnName)
{
if (resultMessage.type != Message.Type.RESULT || resultMessage.kind != ResultMessage.Kind.ROWS)
fail("Expected RESULT type and ROWS kind, got " + resultMessage.type + " and " + resultMessage.kind);
ResultMessage.Rows rows = ((ResultMessage.Rows) resultMessage);
assertNotNull(rows.result);
assertFalse(rows.result.isEmpty());
assertEquals(1, rows.result.rows.size());
assertEquals(1, rows.result.metadata.names.size());
assertEquals(UTF8Type.instance.asCQL3Type(), rows.result.metadata.names.get(0).type.asCQL3Type());
assertEquals(columnName, rows.result.metadata.names.get(0).name.toString());
List<ByteBuffer> byteBuffer = rows.result.rows.get(0);
assertNotNull(byteBuffer);
assertEquals(1, byteBuffer.size());
return UTF8Type.instance.getSerializer().deserialize(byteBuffer.get(0));
}
protected Pair<String, String> extractPasswordAndRoleName(ResultMessage resultMessage)
{
if (resultMessage.type != Message.Type.RESULT || resultMessage.kind != ResultMessage.Kind.ROWS)
fail("Expected RESULT type and ROWS kind, got " + resultMessage.type + " and " + resultMessage.kind);
ResultMessage.Rows rows = ((ResultMessage.Rows) resultMessage);
assertNotNull(rows.result);
assertFalse(rows.result.isEmpty());
assertEquals(1, rows.result.rows.size());
assertEquals(2, rows.result.metadata.names.size());
assertEquals(UTF8Type.instance.asCQL3Type(), rows.result.metadata.names.get(0).type.asCQL3Type());
assertEquals(UTF8Type.instance.asCQL3Type(), rows.result.metadata.names.get(1).type.asCQL3Type());
assertEquals("generated_password", rows.result.metadata.names.get(0).name.toString());
assertEquals("generated_role_name", rows.result.metadata.names.get(1).name.toString());
List<ByteBuffer> row = rows.result.rows.get(0);
assertNotNull(row);
assertEquals(2, row.size());
String password = UTF8Type.instance.getSerializer().deserialize(row.get(0));
String roleName = UTF8Type.instance.getSerializer().deserialize(row.get(1));
return Pair.create(password, roleName);
}
}

View File

@ -69,7 +69,6 @@ public class CassandraPasswordGeneratorTest
CassandraPasswordValidator validator = new CassandraPasswordValidator(config); CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
assertEquals(20, generator.generate(validator).length()); assertEquals(20, generator.generate(validator).length());
assertEquals(30, generator.generate(30, validator).length());
} }
@Test @Test

View File

@ -393,13 +393,17 @@ public class CassandraPasswordValidatorTest
public void testMaxLength() public void testMaxLength()
{ {
CustomGuardrailConfig config = new CustomGuardrailConfig(); CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(CassandraPasswordConfiguration.LENGTH_WARN_KEY, 999);
CassandraPasswordValidator validator = new CassandraPasswordValidator(config); CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
CassandraPasswordGenerator generator = new CassandraPasswordGenerator(config); CassandraPasswordGenerator generator = new CassandraPasswordGenerator(config);
String password = generator.generate(1000, validator); String password = generator.generate(validator);
assertEquals(1000, password.length()); assertEquals(999, password.length());
assertThatThrownBy(() -> generator.generate(1001, validator)) CustomGuardrailConfig config2 = new CustomGuardrailConfig();
config2.put(CassandraPasswordConfiguration.LENGTH_WARN_KEY, 1001);
assertThatThrownBy(() -> new CassandraPasswordGenerator(config2))
.isInstanceOf(ConfigurationException.class) .isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Unable to generate a password of length " + 1001); .hasMessageContaining(MAX_LENGTH_KEY + " of value " + CassandraPasswordConfiguration.MAX_LENGTH +
" is less or equal to " + LENGTH_WARN_KEY + " of value 1001");
} }
} }

View File

@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import org.junit.Test;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.utils.Pair;
import static java.lang.String.format;
import static org.apache.cassandra.db.guardrails.ValueGenerator.GENERATOR_CLASS_NAME_KEY;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests mixture of role name and password generators.
*/
public class GenerationalGuardrailsTest extends AbstractGenerationalTest
{
@Test
public void testMixedGeneration()
{
configureGuardrails();
ValueGenerator<String> passwordGenerator = Guardrails.passwordPolicy.getGenerator();
ValueGenerator<String> roleNameGenerator = Guardrails.roleNamePolicy.getGenerator();
Guardrails.passwordPolicy.validate(extractGeneratedRoleName(execute(userClientState, format("CREATE GENERATED ROLE WITH PASSWORD = '%s'", passwordGenerator.generate()))), userClientState);
Guardrails.roleNamePolicy.validate(extractGeneratedPassword(execute(userClientState, format("CREATE ROLE %s WITH GENERATED PASSWORD", roleNameGenerator.generate()))), userClientState);
}
@Test
public void testUsageWhenBothEnabled()
{
assertThatThrownBy(() -> execute(userClientState, "CREATE GENERATED ROLE WITH GENERATED PASSWORD"))
.isInstanceOf(InvalidRequestException.class)
.hasMessage("You have to enable role_name_policy and its generator_class_name property in cassandra.yaml to be able to generate role names.");
configureRoleNamePolicy();
assertThatThrownBy(() -> execute(userClientState, "CREATE GENERATED ROLE WITH GENERATED PASSWORD"))
.isInstanceOf(InvalidRequestException.class)
.hasMessage("You have to enable password_policy and its generator_class_name property in cassandra.yaml to be able to generate passwords.");
configurePasswordPolicy();
Pair<String, String> generatedPasswordAndRole = extractPasswordAndRoleName(execute(userClientState, "CREATE GENERATED ROLE WITH GENERATED PASSWORD"));
Guardrails.passwordPolicy.validate(generatedPasswordAndRole.left, userClientState);
Guardrails.roleNamePolicy.validate(generatedPasswordAndRole.right, userClientState);
}
private void configureGuardrails()
{
configureRoleNamePolicy();
configurePasswordPolicy();
}
private void configureRoleNamePolicy()
{
CustomGuardrailConfig roleNamePolicyConfig = new CustomGuardrailConfig();
roleNamePolicyConfig.put(GENERATOR_CLASS_NAME_KEY, UUIDRoleNameGenerator.class.getName());
setRoleNamePolicy(roleNamePolicyConfig);
}
private void configurePasswordPolicy()
{
CustomGuardrailConfig passwordPolicyConfig = new CustomGuardrailConfig();
passwordPolicyConfig.put(GENERATOR_CLASS_NAME_KEY, CassandraPasswordGenerator.class.getName());
setPasswordPolicy(passwordPolicyConfig);
}
}

View File

@ -18,17 +18,13 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.transport.Message;
import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.transport.messages.ResultMessage;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
@ -36,41 +32,69 @@ import static java.lang.String.format;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LENGTH_FAIL_KEY; import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LENGTH_FAIL_KEY;
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LENGTH_WARN_KEY; import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LENGTH_WARN_KEY;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class GuardrailPasswordTest extends GuardrailTester public class GuardrailPasswordPolicyTest extends AbstractGenerationalTest
{ {
private void setGuardrail(Map<String, Object> config)
{
guardrails().reconfigurePasswordValidator(config);
}
@After
public void afterTest() throws Throwable
{
// disable guardrail
setGuardrail(Map.of());
}
private String entity = "ROLE";
@Test @Test
public void testPasswordGuardrailForRole() throws Throwable public void testPasswordGuardrail() throws Throwable
{ {
testPasswordGuardrailInternal(); // test that by default there is no password guardrail by creating a user with some invalid password
} executeRoleStatement(true, "role1", "abcdefgh", false, null, null, true);
@Test // enable password guardrail
public void testPasswordGuardrailForUser() throws Throwable setPasswordPolicy(getConfig(true));
{
entity = "USER"; // test that creation of a role with invalid password does not work anymore
testPasswordGuardrailInternal(); executeRoleStatement(true, "role2", "abcdefgh",
true,
"Password was not set as it violated configured password strength policy.",
"[INSUFFICIENT_DIGIT, INSUFFICIENT_CHARACTERISTICS, TOO_SHORT, ILLEGAL_ALPHABETICAL_SEQUENCE, INSUFFICIENT_SPECIAL, INSUFFICIENT_UPPERCASE]}",
true);
PasswordPolicyGuardrail guardrail = new PasswordPolicyGuardrail(() -> new CustomGuardrailConfig(getConfig(true)));
String userGeneratedPassword = guardrail.generate(userClientState, Map.of());
executeRoleStatement(true, "role3", userGeneratedPassword, false, null, null, true);
// altering role3 with valid password should work
Optional<ResultMessage> resultMessage = executeRoleStatement(false, "role3", userGeneratedPassword, false, null, null, true);
// there is no password provided in the response as we set ours
assertTrue(resultMessage.isEmpty());
// alter role3 with generated password
Optional<ResultMessage> resultMessage2 = executeRoleStatement(false, "role3", null, false, null, null, true);
// we have not passed our own password, so it will be generated and returned to us
assertTrue(resultMessage2.isPresent());
String cassandraGeneratedPassword = extractGeneratedPassword(resultMessage2.get());
// verify that they are valid
assertTrue(guardrail.getValidator().shouldWarn(cassandraGeneratedPassword, superClientState.isSuper()).isEmpty());
assertTrue(guardrail.getValidator().shouldFail(cassandraGeneratedPassword, superClientState.isSuper()).isEmpty());
// reconfigure it so it requires 25/20 length
CustomGuardrailConfig config = getConfig(true);
config.put(LENGTH_FAIL_KEY, 20);
config.put(LENGTH_WARN_KEY, 25);
setPasswordPolicy(config);
executeRoleStatement(true, "role4",
// password which is size 24, that should emit warning now
"L:2@5*N|9_%L[\\SGMt9F`]Ko",
false,
"Guardrail password_policy violated: Password was set, however it might not be strong enough according to the configured password strength policy. " +
"To fix this warning, the following has to be resolved: Password must be 25 or more characters in length. " +
"You may also use 'GENERATED PASSWORD' upon role creation or alteration.",
"[TOO_SHORT]",
false);
// disable password guardrail
setPasswordPolicy(getConfig(false));
// without guardrail, we can create roles with whatever passwords again
executeRoleStatement(true, "role5", "abcdefgh", false, null, null, true);
} }
/** /**
@ -81,7 +105,7 @@ public class GuardrailPasswordTest extends GuardrailTester
{ {
// this should generate a password automatically, but it will fail because there is no configured generator // this should generate a password automatically, but it will fail because there is no configured generator
executeRoleStatement(true, "role6", null, true, executeRoleStatement(true, "role6", null, true,
"You have to enable password_validator and it's generator_class_name property " + "You have to enable password_policy and its generator_class_name property " +
"in cassandra.yaml to be able to generate passwords.", "in cassandra.yaml to be able to generate passwords.",
null, true); null, true);
} }
@ -93,7 +117,7 @@ public class GuardrailPasswordTest extends GuardrailTester
public void testGeneratedAndHashedPasswordCannotBeUsedTogether() throws Throwable public void testGeneratedAndHashedPasswordCannotBeUsedTogether() throws Throwable
{ {
// enable password guardrail // enable password guardrail
setGuardrail(getConfig(true)); setPasswordPolicy(getConfig(true));
String hashedPassword = BCrypt.hashpw("doesnotmatter", BCrypt.gensalt(10)); String hashedPassword = BCrypt.hashpw("doesnotmatter", BCrypt.gensalt(10));
@ -109,10 +133,10 @@ public class GuardrailPasswordTest extends GuardrailTester
@Test @Test
public void testAllSpecialCharactersArePossibleToUseInCQLQuery() public void testAllSpecialCharactersArePossibleToUseInCQLQuery()
{ {
setGuardrail(getConfig(true)); setPasswordPolicy(getConfig(true));
PasswordGuardrail guardrail = new PasswordGuardrail(() -> new CustomGuardrailConfig(getConfig(true))); PasswordPolicyGuardrail guardrail = new PasswordPolicyGuardrail(() -> new CustomGuardrailConfig(getConfig(true)));
String userGeneratedPassword = guardrail.generate(20); String userGeneratedPassword = guardrail.generate(userClientState, Map.of());
String allSpecialCharacters = CassandraPasswordValidator.specialCharacters.getCharacters(); String allSpecialCharacters = CassandraPasswordValidator.specialCharacters.getCharacters();
String passwordWithAllSpecialChars = userGeneratedPassword + allSpecialCharacters; String passwordWithAllSpecialChars = userGeneratedPassword + allSpecialCharacters;
@ -120,90 +144,6 @@ public class GuardrailPasswordTest extends GuardrailTester
execute(userClientState, queryToExecute); execute(userClientState, queryToExecute);
} }
private String getEntityName(String name)
{
return toLowerCaseLocalized(name + entity);
}
private void testPasswordGuardrailInternal() throws Throwable
{
// test that by default there is no password guardrail by creating a user with some invalid password
executeRoleStatement(true, getEntityName("role1"), "abcdefgh", false, null, null, true);
// enable password guardrail
setGuardrail(getConfig(true));
// test that creation of a role with invalid password does not work anymore
executeRoleStatement(true, getEntityName("role2"), "abcdefgh",
true,
"Password was not set as it violated configured password strength policy.",
"[INSUFFICIENT_DIGIT, INSUFFICIENT_CHARACTERISTICS, TOO_SHORT, ILLEGAL_ALPHABETICAL_SEQUENCE, INSUFFICIENT_SPECIAL, INSUFFICIENT_UPPERCASE]}",
true);
PasswordGuardrail guardrail = new PasswordGuardrail(() -> new CustomGuardrailConfig(getConfig(true)));
String userGeneratedPassword = guardrail.generate(20);
executeRoleStatement(true, getEntityName("role3"), userGeneratedPassword, false, null, null, true);
// altering role3 with valid password should work
Optional<ResultMessage> resultMessage = executeRoleStatement(false, getEntityName("role3"), userGeneratedPassword, false, null, null, true);
// there is no password provided in the response as we set ours
assertTrue(resultMessage.isEmpty());
// alter role3 with generated password
Optional<ResultMessage> resultMessage2 = executeRoleStatement(false, getEntityName("role3"), null, false, null, null, true);
// we have not passed our own password, so it will be generated and returned to us
assertTrue(resultMessage2.isPresent());
String cassandraGeneratedPassword = extractGeneratedPassword(resultMessage2.get());
// verify that they are valid
assertTrue(guardrail.getValidator().shouldWarn(cassandraGeneratedPassword, superClientState.isSuper()).isEmpty());
assertTrue(guardrail.getValidator().shouldFail(cassandraGeneratedPassword, superClientState.isSuper()).isEmpty());
// reconfigure it so it requires 25/20 length
CustomGuardrailConfig config = getConfig(true);
config.put(LENGTH_FAIL_KEY, 20);
config.put(LENGTH_WARN_KEY, 25);
setGuardrail(config);
// generate a new password which will be of size 21, that should emit warning now
userGeneratedPassword = guardrail.generate(21);
executeRoleStatement(true, getEntityName("role4"),
userGeneratedPassword,
false,
"Guardrail password violated: Password was set, however it might not be strong enough according to the configured password strength policy. " +
"To fix this warning, the following has to be resolved: Password must be 25 or more characters in length. " +
"You may also use 'GENERATED PASSWORD' upon role creation or alteration.",
"[TOO_SHORT]",
false);
// disable password guardrail
setGuardrail(getConfig(false));
// without guardrail, we can create roles with whatever passwords again
executeRoleStatement(true, getEntityName("role5"), "abcdefgh", false, null, null, true);
}
private String extractGeneratedPassword(ResultMessage resultMessage)
{
if (resultMessage.type != Message.Type.RESULT || resultMessage.kind != ResultMessage.Kind.ROWS)
fail("Expected RESULT type and ROWS kind, got " + resultMessage.type + " and " + resultMessage.kind);
ResultMessage.Rows rows = ((ResultMessage.Rows) resultMessage);
assertNotNull(rows.result);
assertFalse(rows.result.isEmpty());
assertEquals(1, rows.result.rows.size());
assertEquals(1, rows.result.metadata.names.size());
assertEquals(UTF8Type.instance.asCQL3Type(), rows.result.metadata.names.get(0).type.asCQL3Type());
assertEquals("generated_password", rows.result.metadata.names.get(0).name.toString());
List<ByteBuffer> passwordByteBuffer = rows.result.rows.get(0);
assertNotNull(passwordByteBuffer);
assertEquals(1, passwordByteBuffer.size());
return UTF8Type.instance.getSerializer().deserialize(passwordByteBuffer.get(0));
}
private Optional<ResultMessage> executeRoleStatement(boolean create, private Optional<ResultMessage> executeRoleStatement(boolean create,
String roleName, String roleName,
String password, String password,
@ -212,23 +152,7 @@ public class GuardrailPasswordTest extends GuardrailTester
String expectedRedactedMessage, String expectedRedactedMessage,
boolean assertFails) throws Throwable boolean assertFails) throws Throwable
{ {
String query; final String queryToExecute = getQueryToExecute(create, roleName, password);
if (password == null)
{
if (create)
query = format("CREATE " + entity + " %s WITH GENERATED PASSWORD", roleName);
else
query = format("ALTER " + entity + " %s WITH GENERATED PASSWORD", roleName);
}
else
{
if (create)
query = format("CREATE " + entity + " %s WITH PASSWORD %s", roleName, entity.equals("ROLE") ? "= " : "") + '\'' + password + '\'';
else
query = format("ALTER " + entity + " %s WITH PASSWORD %s", roleName, entity.equals("ROLE") ? "= " : "") + '\'' + password + '\'';
}
final String queryToExecute = query;
if (assertFails) if (assertFails)
{ {
return assertFails(() -> execute(userClientState, queryToExecute), return assertFails(() -> execute(userClientState, queryToExecute),
@ -244,13 +168,31 @@ public class GuardrailPasswordTest extends GuardrailTester
} }
} }
private static String getQueryToExecute(boolean create, String roleName, String password)
{
if (password == null)
{
if (create)
return format("CREATE ROLE %s WITH GENERATED PASSWORD", roleName);
else
return format("ALTER ROLE %s WITH GENERATED PASSWORD", roleName);
}
else
{
if (create)
return format("CREATE ROLE %s WITH PASSWORD = '%s'", roleName, password);
else
return format("ALTER ROLE %s WITH PASSWORD = '%s'", roleName, password);
}
}
private CustomGuardrailConfig getConfig(boolean validate) private CustomGuardrailConfig getConfig(boolean validate)
{ {
if (validate) if (validate)
{ {
CustomGuardrailConfig config = new CassandraPasswordConfiguration(new CustomGuardrailConfig()).asCustomGuardrailConfig(); CustomGuardrailConfig config = new CassandraPasswordConfiguration(new CustomGuardrailConfig()).asCustomGuardrailConfig();
config.put(ValueValidator.CLASS_NAME_KEY, CassandraPasswordValidator.class.getName()); config.put(ValueValidator.VALIDATOR_CLASS_NAME_KEY, CassandraPasswordValidator.class.getName());
config.put(ValueGenerator.GENERATOR_CLASS_NAME_KEY, CassandraPasswordGenerator.class.getName()); config.put(ValueGenerator.GENERATOR_CLASS_NAME_KEY, CassandraPasswordGenerator.class.getName());
config.put(LENGTH_FAIL_KEY, 15); config.put(LENGTH_FAIL_KEY, 15);

View File

@ -0,0 +1,182 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.regex.Pattern;
import org.junit.Test;
import org.apache.cassandra.auth.CassandraRoleManager;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.SyntaxException;
import static java.util.Collections.singletonList;
import static org.apache.cassandra.db.guardrails.ValueGenerator.GENERATOR_CLASS_NAME_KEY;
import static org.apache.cassandra.db.guardrails.ValueValidator.VALIDATOR_CLASS_NAME_KEY;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class GuardrailRoleNamePolicyTest extends AbstractGenerationalTest
{
@Test
public void testRoleNameValidation() throws Throwable
{
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(VALIDATOR_CLASS_NAME_KEY, TestRoleNameValidator.class.getName());
setRoleNamePolicy(config);
assertFails(() -> execute(userClientState, "CREATE ROLE withnumber1"),
false,
singletonList("Role name is not alphanumeric."),
singletonList("Role name is not alphanumeric."));
assertFails(() -> execute(userClientState, "CREATE ROLE notalphanumeric_"),
true,
singletonList("Role name is not alphanumeric."),
singletonList("Role name is not alphanumeric."));
}
/**
* Test what happens if generator generates invalid value for validator
* This is normally treated in GuardrailsOptions constructor as a test it done there,
* but it is the best-effort, as it might still generate something invalid eventually ...
*/
@Test
public void testInvalidRoleNameGeneration() throws Throwable
{
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(VALIDATOR_CLASS_NAME_KEY, TestRoleNameValidator.class.getName());
config.put(GENERATOR_CLASS_NAME_KEY, InvalidRoleNameGenerator.class.getName());
setRoleNamePolicy(config);
assertFails(() -> execute(userClientState, "CREATE GENERATED ROLE"),
true,
singletonList("Role name is not alphanumeric."),
singletonList("Role name is not alphanumeric."));
}
@Test
public void testInvalidCombinations()
{
assertThatThrownBy(() -> execute(userClientState, "CREATE GENERATED ROLE somerole"))
.isInstanceOf(SyntaxException.class)
.hasMessage("Name can not be specified together with GENERATED keyword.");
}
@Test
public void testRoleNameGenerationWhenNotEnabled()
{
assertThatThrownBy(() -> execute(userClientState, "CREATE GENERATED ROLE"))
.isInstanceOf(InvalidRequestException.class)
.hasMessage("You have to enable role_name_policy and its generator_class_name property in cassandra.yaml to be able to generate role names.");
}
@Test
public void testRoleNameGenerationWithOptions() throws Throwable
{
IRoleManager oldManager = DatabaseDescriptor.getRoleManager();
try
{
setup();
String withPrefix = extractGeneratedRoleName(execute(userClientState,
"CREATE GENERATED ROLE WITH OPTIONS = {'name_prefix': 'company_'}"));
assertTrue(withPrefix.startsWith("company_"));
///
String query = "CREATE GENERATED ROLE WITH OPTIONS = {'name_suffix': '_read_only'}";
String withSuffix = extractGeneratedRoleName(execute(userClientState, query));
assertTrue(withSuffix.endsWith("_read_only"));
///
query = "CREATE GENERATED ROLE WITH OPTIONS = {'name_prefix': 'company_', 'name_suffix': '_read_only'}";
String withPrefixAndSuffix = extractGeneratedRoleName(execute(userClientState, query));
assertTrue(withPrefixAndSuffix.startsWith("company_"));
assertTrue(withPrefixAndSuffix.endsWith("_read_only"));
///
query = "CREATE GENERATED ROLE WITH OPTIONS = {'name_prefix': 'company_', 'name_suffix': '_read_only', 'name_size': 10}";
String withPrefixAndSuffixAndSize = extractGeneratedRoleName(execute(userClientState, query));
assertTrue(withPrefixAndSuffixAndSize.startsWith("company_"));
assertTrue(withPrefixAndSuffixAndSize.endsWith("_read_only"));
Pattern pattern1 = Pattern.compile("company_");
Pattern pattern2 = Pattern.compile("_read_only");
String withoutPrefix = pattern1.matcher(withPrefixAndSuffixAndSize).replaceAll("");
String onlyName = pattern2.matcher(withoutPrefix).replaceAll("");
assertEquals(10, onlyName.length());
///
query = "CREATE GENERATED ROLE WITH OPTIONS = {'name_size': 10}";
assertEquals(10, extractGeneratedRoleName(execute(userClientState, query)).length());
assertFails(() -> execute(userClientState, "CREATE GENERATED ROLE WITH OPTIONS = {'name_size': 6}"),
true,
singletonList("Value of name_size parameter has to be at least 10."),
singletonList("Value of name_size parameter has to be at least 10."));
}
finally
{
DatabaseDescriptor.setRoleManager(oldManager);
}
}
@Test
public void testRoleGenerationWithIfNotExists()
{
IRoleManager oldManager = DatabaseDescriptor.getRoleManager();
try
{
setup();
assertThatThrownBy(() -> execute(userClientState, "CREATE GENERATED ROLE IF NOT EXISTS"))
.isInstanceOf(SyntaxException.class)
.hasMessage("GENERATED keyword for role creation can not be used together with IF NOT EXISTS.");
}
finally
{
DatabaseDescriptor.setRoleManager(oldManager);
}
}
private void setup()
{
// we need to have this set first so CassandraRoleManager sees we do not use NoOP,
// so it will add OPTIONS among supported ones for us to be able to specify OPTIONS in queries
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(GENERATOR_CLASS_NAME_KEY, UUIDRoleNameGenerator.class.getName());
setRoleNamePolicy(config);
CassandraRoleManager cassandraRoleManager = new CassandraRoleManager();
cassandraRoleManager.setup(false);
DatabaseDescriptor.setRoleManager(cassandraRoleManager);
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Map;
import javax.annotation.Nonnull;
import org.apache.cassandra.exceptions.ConfigurationException;
/**
* This will generate a role name which will fail when validated with {@link TestRoleNameValidator}.
*/
public class InvalidRoleNameGenerator extends ValueGenerator<String>
{
public InvalidRoleNameGenerator()
{
this(new CustomGuardrailConfig());
}
public InvalidRoleNameGenerator(CustomGuardrailConfig config)
{
super(config);
}
@Override
public String generate(ValueValidator<String> validator, Map<String, Object> options)
{
return "non_alpha_numeric";
}
@Nonnull
@Override
public CustomGuardrailConfig getParameters()
{
return config;
}
@Override
public void validateParameters() throws ConfigurationException
{
}
}

View File

@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Optional;
import org.junit.Test;
import org.apache.cassandra.db.guardrails.ValueValidator.ValidationViolation;
import static org.apache.cassandra.db.guardrails.ValueValidator.VALIDATOR_CLASS_NAME_KEY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class RoleNameValidatorTest
{
@Test
public void testRoleNameValidator()
{
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(VALIDATOR_CLASS_NAME_KEY, TestRoleNameValidator.class.getName());
ValueValidator<Object> validator = ValueValidator.getValidator("role_name_policy", config);
assertSame(TestRoleNameValidator.class.getName(), validator.getClass().getName());
Optional<ValidationViolation> warningBecauseOfNumber = validator.shouldWarn("user123", false);
assertTrue(warningBecauseOfNumber.isPresent());
assertEquals("Role name contains a number.", warningBecauseOfNumber.get().message);
Optional<ValidationViolation> errorBecauseOfSpecialChar = validator.shouldFail("user$", false);
assertTrue(errorBecauseOfSpecialChar.isPresent());
assertEquals("Role name is not alphanumeric.", errorBecauseOfSpecialChar.get().message);
}
}

View File

@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Optional;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.apache.cassandra.exceptions.ConfigurationException;
/**
* For test purposes, it will warn everything which has a number in it,
* and it will fail if it is not alphanumeric.
*/
public class TestRoleNameValidator extends ValueValidator<String>
{
public TestRoleNameValidator()
{
this(new CustomGuardrailConfig());
}
public TestRoleNameValidator(CustomGuardrailConfig config)
{
super(config);
}
@Override
public Optional<ValidationViolation> shouldWarn(String string, boolean calledBySuperuser)
{
boolean containsNumber = StringUtils.containsAny(string, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0');
if (containsNumber)
return Optional.of(new ValidationViolation("Role name contains a number."));
return Optional.empty();
}
@Override
public Optional<ValidationViolation> shouldFail(String string, boolean calledBySuperUser)
{
if (!StringUtils.isAlphanumeric(string))
return Optional.of(new ValidationViolation("Role name is not alphanumeric."));
return Optional.empty();
}
@Override
public void validateParameters() throws ConfigurationException
{
}
@Nonnull
@Override
public CustomGuardrailConfig getParameters()
{
return config;
}
}

View File

@ -0,0 +1,206 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.apache.cassandra.exceptions.ConfigurationException;
import static java.util.Map.of;
import static org.apache.cassandra.db.guardrails.UUIDRoleNameGenerator.DEFAULT_MINIMUM_NAME_SIZE;
import static org.apache.cassandra.db.guardrails.UUIDRoleNameGenerator.MAXIMUM_NAME_SIZE;
import static org.apache.cassandra.db.guardrails.UUIDRoleNameGenerator.MINIMUM_NAME_SIZE_CONFIG_OPTION;
import static org.apache.cassandra.db.guardrails.UUIDRoleNameGenerator.NAME_PREFIX_KEY;
import static org.apache.cassandra.db.guardrails.UUIDRoleNameGenerator.NAME_SIZE;
import static org.apache.cassandra.db.guardrails.UUIDRoleNameGenerator.NAME_SUFFIX_KEY;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class UUIDRoleNameGeneratorTest
{
@Test
public void testRoleNameGenerator()
{
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator().generate(), MAXIMUM_NAME_SIZE);
assertTrue(new UUIDRoleNameGenerator()
.generate(of(NAME_PREFIX_KEY, "this_is_prefix_"))
.startsWith("this_is_prefix_"));
assertTrue(new UUIDRoleNameGenerator()
.generate(of(NAME_SUFFIX_KEY, "_this_is_suffix"))
.endsWith("_this_is_suffix"));
String generatedPassword = new UUIDRoleNameGenerator().generate(of(NAME_PREFIX_KEY,
"this_is_prefix_",
NAME_SUFFIX_KEY,
"_this_is_suffix"));
assertTrue(generatedPassword.endsWith("_this_is_suffix"));
assertTrue(generatedPassword.startsWith("this_is_prefix_"));
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator().generate(of(NAME_SIZE, 15)), 15);
// more than 32 errors out
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(of(NAME_SIZE, 45)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Generator generates names of maximum length " + MAXIMUM_NAME_SIZE + ". You want to generate with length 45.");
// assert edge cases
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator().generate(of(NAME_SIZE, MAXIMUM_NAME_SIZE)), MAXIMUM_NAME_SIZE);
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator().generate(of(NAME_SIZE, DEFAULT_MINIMUM_NAME_SIZE)), DEFAULT_MINIMUM_NAME_SIZE);
// when configured minimum size of 15, less than 15 errors out
CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(MINIMUM_NAME_SIZE_CONFIG_OPTION, 15);
assertThatThrownBy(() -> new UUIDRoleNameGenerator(config).generate(of(NAME_SIZE, 13)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of name_size parameter has to be at least 15.");
// same as above but try to configure edge
CustomGuardrailConfig config2 = new CustomGuardrailConfig();
config2.put(MINIMUM_NAME_SIZE_CONFIG_OPTION, 15);
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator(config2).generate(of(NAME_SIZE, 15)), 15);
// configuring minimum name size less than 10 errors out
CustomGuardrailConfig config3 = new CustomGuardrailConfig();
config3.put(MINIMUM_NAME_SIZE_CONFIG_OPTION, 8);
assertThatThrownBy(() -> new UUIDRoleNameGenerator(config3))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(MINIMUM_NAME_SIZE_CONFIG_OPTION + " has to be at least " + DEFAULT_MINIMUM_NAME_SIZE + " and at most " + MAXIMUM_NAME_SIZE);
CustomGuardrailConfig config4 = new CustomGuardrailConfig();
config4.put(MINIMUM_NAME_SIZE_CONFIG_OPTION, 55);
assertThatThrownBy(() -> new UUIDRoleNameGenerator(config4))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining(MINIMUM_NAME_SIZE_CONFIG_OPTION + " has to be at least " + DEFAULT_MINIMUM_NAME_SIZE + " and at most " + MAXIMUM_NAME_SIZE);
}
@Test
public void testOptions()
{
// integer
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator().generate(of(NAME_SIZE, 15)), 15);
// length as string
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator().generate(of(NAME_SIZE, "15")), 15);
// length as float, will be converted to string
assertGeneratedRoleNameSize(new UUIDRoleNameGenerator().generate(of(NAME_SIZE, 15f)), 15);
// invalid number
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(of(NAME_SIZE, "invalid number")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value 'invalid number' can't be converted to integer.");
// negative number as string
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(of(NAME_SIZE, "-1")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of name_size parameter has to be at least 10.");
// negative number as integer
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(of(NAME_SIZE, -1)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of name_size parameter has to be at least 10.");
// name_size has to be string or integer
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(Map.of(NAME_SIZE, new Object())))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Unsupported object passed to resolve " + NAME_SIZE + ": " + Object.class.getName());
// null name_size
Map<String, Object> configMap = new HashMap<>();
configMap.put(NAME_SIZE, null);
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(configMap))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of name_size has to be strictly positive integer.");
// null prefix or suffix
Map<String, Object> configMap2 = new HashMap<>();
configMap2.put(NAME_PREFIX_KEY, null);
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(configMap2))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of " + NAME_PREFIX_KEY + " cannot be null.");
Map<String, Object> configMap3 = new HashMap<>();
configMap3.put(NAME_SUFFIX_KEY, null);
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(configMap3))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of " + NAME_SUFFIX_KEY + " cannot be null.");
// suffix and prefix has to be a string
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(Map.of(NAME_SUFFIX_KEY, 1)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of " + NAME_SUFFIX_KEY + " is not a string.");
assertThatThrownBy(() -> new UUIDRoleNameGenerator().generate(Map.of(NAME_PREFIX_KEY, 1)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Value of " + NAME_PREFIX_KEY + " is not a string.");
}
@Test
public void testGeneratedRoleNameAlwaysStartsWithALetter()
{
UUIDRoleNameGenerator generator = new UUIDRoleNameGenerator();
// a thousand is just enough to see that it "always" starts with a letter
for (int i = 0; i < 1000; i++)
{
String generatedName = generator.generate();
assertNotNull(generatedName);
char firstChar = generatedName.toCharArray()[0];
boolean startsWithLetter = false;
for (int j = 0; j < UUIDRoleNameGenerator.FIRST_CHARS.length; j++)
{
if (UUIDRoleNameGenerator.FIRST_CHARS[j] == firstChar)
{
startsWithLetter = true;
break;
}
}
if (!startsWithLetter)
fail(generatedName + " does not start with a letter.");
}
}
@Test
public void testSupportedOptions()
{
Set<String> supportedOptionsKeys = new UUIDRoleNameGenerator().getSupportedOptionsKeys();
assertTrue(supportedOptionsKeys.contains(NAME_PREFIX_KEY));
assertTrue(supportedOptionsKeys.contains(NAME_SUFFIX_KEY));
assertTrue(supportedOptionsKeys.contains(NAME_SIZE));
}
private void assertGeneratedRoleNameSize(String roleName, int size)
{
assertNotNull(roleName);
assertEquals(size, roleName.length());
}
}

View File

@ -19,6 +19,7 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import org.junit.Test; import org.junit.Test;
@ -47,7 +48,9 @@ public class ValueGeneratorTest
assertNotNull(defaultBooleans); assertNotNull(defaultBooleans);
assertEquals(10, defaultBooleans.length); assertEquals(10, defaultBooleans.length);
Boolean[] twentyBooleans = booleanGenerator.generate(20, booleanValidator); config.put("default_size", 20);
booleanGenerator = ValueGenerator.getGenerator("boolean generator", config);
Boolean[] twentyBooleans = booleanGenerator.generate(booleanValidator, Map.of());
assertEquals(20, twentyBooleans.length); assertEquals(20, twentyBooleans.length);
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
assertEquals(true, twentyBooleans[i]); assertEquals(true, twentyBooleans[i]);
@ -77,14 +80,6 @@ public class ValueGeneratorTest
defaultSize = (Integer) config.get("default_size"); defaultSize = (Integer) config.get("default_size");
} }
@Override
public Boolean[] generate(int size, ValueValidator<Boolean[]> validator)
{
Boolean[] booleans = new Boolean[size];
Arrays.fill(booleans, expectingTrue);
return booleans;
}
@Override @Override
public Boolean[] generate(ValueValidator<Boolean[]> validator) public Boolean[] generate(ValueValidator<Boolean[]> validator)
{ {
@ -93,6 +88,12 @@ public class ValueGeneratorTest
return booleans; return booleans;
} }
@Override
public Boolean[] generate(ValueValidator<Boolean[]> validator, Map<String, Object> options)
{
return generate(validator);
}
@Nonnull @Nonnull
@Override @Override
public CustomGuardrailConfig getParameters() public CustomGuardrailConfig getParameters()

View File

@ -28,7 +28,7 @@ import org.apache.cassandra.exceptions.ConfigurationException;
import static java.lang.Boolean.FALSE; import static java.lang.Boolean.FALSE;
import static java.lang.String.format; import static java.lang.String.format;
import static org.apache.cassandra.db.guardrails.ValueValidator.CLASS_NAME_KEY; import static org.apache.cassandra.db.guardrails.ValueValidator.VALIDATOR_CLASS_NAME_KEY;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -60,7 +60,7 @@ public class ValueValidatorTest
public void testValidatorCreation() public void testValidatorCreation()
{ {
CustomGuardrailConfig config = new CustomGuardrailConfig(); CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(CLASS_NAME_KEY, BooleanValidator.class.getName()); config.put(VALIDATOR_CLASS_NAME_KEY, BooleanValidator.class.getName());
config.put("expecting_true", FALSE); config.put("expecting_true", FALSE);
ValueValidator<Boolean> booleanValidator = ValueValidator.getValidator("boolean validator", config); ValueValidator<Boolean> booleanValidator = ValueValidator.getValidator("boolean validator", config);
@ -75,7 +75,7 @@ public class ValueValidatorTest
public void testValidatorCreationWithInvalidConfiguration() public void testValidatorCreationWithInvalidConfiguration()
{ {
CustomGuardrailConfig config = new CustomGuardrailConfig(); CustomGuardrailConfig config = new CustomGuardrailConfig();
config.put(CLASS_NAME_KEY, BooleanValidator.class.getName()); config.put(VALIDATOR_CLASS_NAME_KEY, BooleanValidator.class.getName());
assertThatThrownBy(() -> ValueValidator.getValidator("boolean validator", config)) assertThatThrownBy(() -> ValueValidator.getValidator("boolean validator", config))
.isInstanceOf(ConfigurationException.class) .isInstanceOf(ConfigurationException.class)