mirror of https://github.com/apache/cassandra
Implementation of CEP-55 - Generation of role names
patch by Stefan Miklosovic; reviewed by Bernardo Botella for CASSANDRA-20897
This commit is contained in:
parent
ed12f31f9d
commit
035a220d55
|
|
@ -1,4 +1,5 @@
|
|||
5.1
|
||||
* Implementation of CEP-55 - Generation of role names (CASSANDRA-20897)
|
||||
* 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)
|
||||
* Optimize Counter, Meter and Histogram metrics using thread local counters (CASSANDRA-20250)
|
||||
|
|
|
|||
5
NEWS.txt
5
NEWS.txt
|
|
@ -102,10 +102,13 @@ New features
|
|||
- 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
|
||||
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
|
||||
when 'GENERATED PASSWORD' clause is used. Character sets supported are: English, Cyrillic, modern Cyrillic,
|
||||
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
|
||||
snapshot operations. Snapshot-related methods on StorageServiceMBean are still present and functional
|
||||
but marked as deprecated.
|
||||
|
|
|
|||
|
|
@ -2507,20 +2507,41 @@ drop_compact_storage_enabled: false
|
|||
# This would also apply to system keyspaces.
|
||||
# maximum_replication_factor_warn_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.
|
||||
# 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_validator:
|
||||
#password_policy:
|
||||
# # 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.
|
||||
# class_name: CassandraPasswordValidator
|
||||
# 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.generators is prepended.
|
||||
# # package name org.apache.cassandra.db.guardrails is prepended.
|
||||
# # By default, there is no generator.
|
||||
# generator_class_name: CassandraPasswordGenerator
|
||||
# # 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.
|
||||
# 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_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
|
||||
# and the table is using TimeWindowCompactionStrategy compaction or a subclass of it.
|
||||
|
|
|
|||
|
|
@ -2290,6 +2290,98 @@ drop_compact_storage_enabled: false
|
|||
# maximum_replication_factor_warn_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
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -1329,7 +1329,7 @@ h3(#createRoleStmt). CREATE ROLE
|
|||
__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>
|
||||
| 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 hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
|
||||
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.
|
||||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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> )* )?
|
||||
|
||||
<option> ::= ("HASHED")? PASSWORD = <string>
|
||||
| GENERATED PASSWORD
|
||||
| SUPERUSER
|
||||
| NOSUPERUSER
|
||||
p.
|
||||
|
|
@ -1532,7 +1537,6 @@ __Sample:__
|
|||
bc(sample).
|
||||
CREATE USER alice WITH PASSWORD 'password_a' SUPERUSER;
|
||||
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:
|
||||
|
||||
|
|
@ -1561,7 +1565,6 @@ bc(syntax)..
|
|||
<alter-user-statement> ::= ALTER USER (IF EXISTS)? <identifier> ( WITH <option> ( AND <option> )* )?
|
||||
|
||||
<option> ::= ("HASHED")? PASSWORD = <string>
|
||||
| GENERATED PASSWORD
|
||||
| SUPERUSER
|
||||
| NOSUPERUSER
|
||||
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 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
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@
|
|||
**** xref:cassandra:managing/operating/transientreplication.adoc[Transient replication]
|
||||
**** xref:cassandra:managing/operating/virtualtables.adoc[Virtual tables]
|
||||
**** 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/tools/index.adoc[Tools]
|
||||
**** xref:cassandra:managing/tools/cqlsh.adoc[cqlsh: the CQL shell]
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@
|
|||
* xref:cassandra:managing/operating/transientreplication.adoc[Transient replication]
|
||||
* xref:cassandra:managing/operating/virtualtables.adoc[Virtual tables]
|
||||
* 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[]
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ the password guardrail provides `CassandraPasswordValidator` by extending `Value
|
|||
while passwords are generated by `CassandraPasswordGenerator` by extending `ValueGenerator`.
|
||||
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.
|
||||
Let’s explore the key configuration properties available.
|
||||
|
||||
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.
|
||||
|
||||
----
|
||||
password_validator:
|
||||
password_policy:
|
||||
# Implementation class of a validator. When not in form of FQCN, the
|
||||
# package name org.apache.cassandra.db.guardrails.validators is prepended.
|
||||
# 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;
|
||||
----
|
||||
|
||||
or by `CREATE USER`:
|
||||
|
||||
----
|
||||
cassandra@cqlsh> CREATE USER alice WITH GENERATED PASSWORD;
|
||||
----
|
||||
|
||||
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.
|
||||
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:
|
||||
|
||||
----
|
||||
|
|
@ -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
|
||||
password validator, also configured via `GuardrailsMBean` as any other guardrails. There are two methods exposed:
|
||||
|
||||
* `Map<String, Object> getPasswordValidatorConfig()` - gets password validator configuration
|
||||
* `void reconfigurePasswordValidator(Map<String, Object> config)` - reconfigures the password validator by reading
|
||||
and parsing the configuration from the provided map. Reconfiguration of password validator in runtime is considered
|
||||
* `Map<String, Object> getPasswordPolicy()` - gets password validator configuration as JSON string,
|
||||
* `void setPasswordPolicy(String value)` - reconfigures the password validator by reading
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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#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
|
||||
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].
|
||||
|
||||
== 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]]
|
||||
== Caching
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ COMPLEX_OPTIONS = (
|
|||
'transparent_data_encryption_options',
|
||||
'hinted_handoff_disabled_datacenters',
|
||||
'startup_checks',
|
||||
'password_validator'
|
||||
'password_policy',
|
||||
'role_name_policy'
|
||||
)
|
||||
|
||||
def convert(yaml_file, dest_file):
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
|
|||
| <alterUserStatement>
|
||||
| <dropUserStatement>
|
||||
| <listUsersStatement>
|
||||
| <createGeneratedRoleStatement>
|
||||
| <createRoleStatement>
|
||||
| <alterRoleStatement>
|
||||
| <dropRoleStatement>
|
||||
|
|
@ -1619,6 +1620,10 @@ syntax_rules += r'''
|
|||
| <unreservedKeyword> )
|
||||
;
|
||||
|
||||
<createGeneratedRoleStatement> ::= "CREATE" "GENERATED" "ROLE"
|
||||
( "WITH" <roleProperty> ("AND" <roleProperty>)*)?
|
||||
;
|
||||
|
||||
<createRoleStatement> ::= "CREATE" "ROLE" ("IF" "NOT" "EXISTS")? <rolename>
|
||||
( "WITH" <roleProperty> ("AND" <roleProperty>)*)?
|
||||
;
|
||||
|
|
|
|||
|
|
@ -540,7 +540,7 @@ class Shell(cmd.Cmd):
|
|||
ksname = self.current_keyspace
|
||||
ksmeta = self.get_keyspace_meta(ksname)
|
||||
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)
|
||||
else:
|
||||
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['resource'] = ColumnMetadata(table_meta, 'resource', 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)
|
||||
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_role_name'] = ColumnMetadata(table_meta, 'generated_role_name', cassandra.cqltypes.UTF8Type)
|
||||
else:
|
||||
raise ColumnFamilyNotFound("Column family {} not found".format(tablename))
|
||||
|
||||
|
|
@ -911,16 +912,18 @@ class Shell(cmd.Cmd):
|
|||
if result is None:
|
||||
return False, None
|
||||
|
||||
lowered_query = statement.query_string.lower()
|
||||
|
||||
if statement.query_string[:6].lower() == 'select':
|
||||
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'))
|
||||
elif statement.query_string.lower().startswith("list"):
|
||||
elif lowered_query.startswith("list"):
|
||||
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"):
|
||||
self.print_result(result, self.get_table_meta('system_auth', 'generated_password'))
|
||||
elif statement.query_string.lower().startswith("alter user") or statement.query_string.lower().startswith("alter role"):
|
||||
self.print_result(result, self.get_table_meta('system_auth', 'generated_password'))
|
||||
elif lowered_query.startswith("create role") or lowered_query.startswith("create generated role"):
|
||||
self.print_result(result, self.get_table_meta('system_auth', 'generated_values'))
|
||||
elif lowered_query.startswith("alter role"):
|
||||
self.print_result(result, self.get_table_meta('system_auth', 'generated_values'))
|
||||
elif result:
|
||||
# CAS INSERT/UPDATE
|
||||
self.writeresult("")
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,9 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
|||
def test_complete_in_alter_user(self):
|
||||
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):
|
||||
self.trycompletions('CREATE ROLE ', choices=['<rolename>', 'IF'])
|
||||
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 FROM ', choices=['ALL', '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):
|
||||
self.trycompletions('ALTER ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
|
||||
|
|
|
|||
|
|
@ -1453,15 +1453,8 @@ createUserStatement returns [CreateRoleStatement stmt]
|
|||
{
|
||||
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
||||
}
|
||||
if (opts.getPassword().isPresent() && opts.isGeneratedPassword())
|
||||
{
|
||||
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); }
|
||||
$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:
|
||||
* PASSWORD = '<password>'
|
||||
* GENERATED PASSWORD
|
||||
* SUPERUSER = (true|false)
|
||||
* LOGIN = (true|false)
|
||||
* OPTIONS = { 'k1':'v1', 'k2':'v2'}
|
||||
|
|
@ -1553,8 +1547,9 @@ createRoleStatement returns [CreateRoleStatement stmt]
|
|||
DCPermissions.Builder dcperms = DCPermissions.builder();
|
||||
CIDRPermissions.Builder cidrperms = CIDRPermissions.builder();
|
||||
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] )?
|
||||
{
|
||||
// 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");
|
||||
}
|
||||
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);
|
||||
}
|
||||
;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import org.apache.cassandra.cql3.PasswordObfuscator;
|
|||
import org.apache.cassandra.cql3.QueryEvents;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
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.ConfigurationException;
|
||||
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.";
|
||||
}
|
||||
}
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
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.DurationSpec;
|
||||
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.QueryProcessor;
|
||||
import org.apache.cassandra.cql3.ResultSet;
|
||||
import org.apache.cassandra.cql3.UntypedResultSet;
|
||||
import org.apache.cassandra.cql3.statements.SelectStatement;
|
||||
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.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
|
|
@ -76,6 +82,7 @@ import org.apache.cassandra.utils.NoSpamLogger;
|
|||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
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
|
||||
|
|
@ -164,9 +171,14 @@ public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerM
|
|||
|
||||
public CassandraRoleManager(Map<String, String> parameters)
|
||||
{
|
||||
supportedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
|
||||
? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD)
|
||||
: ImmutableSet.of(Option.LOGIN, Option.SUPERUSER);
|
||||
Set<Option> allowedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
|
||||
? EnumSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD, Option.GENERATED_NAME)
|
||||
: 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
|
||||
? ImmutableSet.of(Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD)
|
||||
: 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)
|
||||
throws RequestValidationException, RequestExecutionException
|
||||
{
|
||||
|
|
@ -827,4 +853,52 @@ public class CassandraRoleManager implements IRoleManager, CassandraRoleManagerM
|
|||
this.invalidClientDisconnectMaxJitterMillis = duration;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.google.common.annotations.VisibleForTesting;
|
|||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
/**
|
||||
* Responsible for managing roles (which also includes what
|
||||
|
|
@ -44,7 +45,7 @@ public interface IRoleManager extends AuthCache.BulkLoader<RoleResource, Set<Rol
|
|||
*/
|
||||
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().
|
||||
*
|
||||
* @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
|
||||
* @throws RequestValidationException
|
||||
* @throws RequestExecutionException
|
||||
|
|
@ -72,6 +73,23 @@ public interface IRoleManager extends AuthCache.BulkLoader<RoleResource, Set<Rol
|
|||
void createRole(AuthenticatedUser performer, RoleResource role, RoleOptions options)
|
||||
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
|
||||
* 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)
|
||||
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.
|
||||
* Grant an role to another existing role. A grantee that has a role granted to it will inherit any
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ public class RoleOptions
|
|||
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 hashed password option value
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ public class RoleResource implements IResource, Comparable<RoleResource>
|
|||
|
||||
private static final String ROOT_NAME = "roles";
|
||||
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 String name;
|
||||
|
|
|
|||
|
|
@ -1003,8 +1003,10 @@ public class Config
|
|||
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_warn_threshold = null;
|
||||
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_fail_threshold = null;
|
||||
|
||||
public volatile boolean password_validator_reconfiguration_enabled = true;
|
||||
public volatile CustomGuardrailConfig password_validator = new CustomGuardrailConfig();
|
||||
public volatile boolean password_policy_reconfiguration_enabled = true;
|
||||
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();
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5923,9 +5923,14 @@ public class DatabaseDescriptor
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
package org.apache.cassandra.config;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
|
@ -108,7 +109,8 @@ public class GuardrailsOptions implements GuardrailsConfig
|
|||
config.sai_sstable_indexes_per_query_fail_threshold,
|
||||
"sai_sstable_indexes_per_query",
|
||||
false);
|
||||
validatePasswordValidator(config.password_validator);
|
||||
validatePasswordPolicy(config.password_policy);
|
||||
validateRoleNamePolicy(config.role_name_policy);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1038,9 +1040,15 @@ public class GuardrailsOptions implements GuardrailsConfig
|
|||
}
|
||||
|
||||
@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)
|
||||
|
|
@ -1538,8 +1546,13 @@ public class GuardrailsOptions implements GuardrailsConfig
|
|||
*
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
*/
|
||||
package org.apache.cassandra.cql3.statements;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cassandra.audit.AuditLogContext;
|
||||
import org.apache.cassandra.audit.AuditLogEntryType;
|
||||
import org.apache.cassandra.auth.AuthenticatedUser;
|
||||
|
|
@ -135,19 +137,20 @@ public class AlterRoleStatement extends AuthenticationStatement
|
|||
|
||||
if (opts.isGeneratedPassword())
|
||||
{
|
||||
String generatedPassword = Guardrails.password.generate();
|
||||
String generatedPassword = Guardrails.passwordPolicy.generate(state, Map.of());
|
||||
if (generatedPassword != null)
|
||||
opts.setOption(IRoleManager.Option.PASSWORD, generatedPassword);
|
||||
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.");
|
||||
}
|
||||
|
||||
if (opts.getPassword().isPresent())
|
||||
Guardrails.password.guard(opts.getPassword().get(), state);
|
||||
Guardrails.passwordPolicy.validate(opts.getPassword().get(), state);
|
||||
|
||||
ResultMessage resultMessage = null;
|
||||
if (!opts.isEmpty())
|
||||
DatabaseDescriptor.getRoleManager().alterRole(state.getUser(), role, opts);
|
||||
resultMessage = DatabaseDescriptor.getRoleManager().alterRoleWithResult(state.getUser(), role, opts);
|
||||
|
||||
if (dcPermissions != null)
|
||||
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
|
||||
|
|
@ -155,7 +158,7 @@ public class AlterRoleStatement extends AuthenticationStatement
|
|||
if (cidrPermissions != null)
|
||||
DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions);
|
||||
|
||||
return getResultMessage(opts);
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -17,36 +17,20 @@
|
|||
*/
|
||||
package org.apache.cassandra.cql3.statements;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.cassandra.auth.Permission;
|
||||
import org.apache.cassandra.auth.RoleOptions;
|
||||
import org.apache.cassandra.auth.RoleResource;
|
||||
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.ResultSet;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.schema.SchemaConstants;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.Dispatcher;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
|
||||
|
||||
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)
|
||||
{
|
||||
return this;
|
||||
|
|
@ -87,21 +71,5 @@ public abstract class AuthenticationStatement extends CQLStatement.Raw implement
|
|||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
*/
|
||||
package org.apache.cassandra.cql3.statements;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cassandra.audit.AuditLogContext;
|
||||
import org.apache.cassandra.audit.AuditLogEntryType;
|
||||
|
||||
|
|
@ -52,7 +54,11 @@ public class CreateRoleStatement extends AuthenticationStatement
|
|||
public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions,
|
||||
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.dcPermissions = dcPermissions;
|
||||
this.cidrPermissions = cidrPermissions;
|
||||
|
|
@ -72,7 +78,6 @@ public class CreateRoleStatement extends AuthenticationStatement
|
|||
public void validate(ClientState state) throws RequestValidationException
|
||||
{
|
||||
opts.validate();
|
||||
|
||||
if (role.getRoleName().isEmpty())
|
||||
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.
|
||||
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()));
|
||||
}
|
||||
|
||||
public ResultMessage execute(ClientState state) throws RequestExecutionException, RequestValidationException
|
||||
{
|
||||
// not rejected in validate()
|
||||
if (ifNotExists && DatabaseDescriptor.getRoleManager().isExistingRole(role))
|
||||
if (ifNotExists && role != RoleResource.GENERATED_ROLE && DatabaseDescriptor.getRoleManager().isExistingRole(role))
|
||||
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())
|
||||
{
|
||||
String generatedPassword = Guardrails.password.generate();
|
||||
String generatedPassword = Guardrails.passwordPolicy.generate(state);
|
||||
if (generatedPassword != null)
|
||||
opts.setOption(IRoleManager.Option.PASSWORD, generatedPassword);
|
||||
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.");
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
|
||||
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(roleResource, dcPermissions);
|
||||
}
|
||||
|
||||
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
|
||||
* see also: AlterTableStatement#createdResources() and the overridden implementations
|
||||
* 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 user is not anonymous
|
||||
|
|
@ -142,8 +167,8 @@ public class CreateRoleStatement extends AuthenticationStatement
|
|||
try
|
||||
{
|
||||
DatabaseDescriptor.getAuthorizer().grant(AuthenticatedUser.SYSTEM_USER,
|
||||
role.applicablePermissions(),
|
||||
role,
|
||||
roleResource.applicablePermissions(),
|
||||
roleResource,
|
||||
RoleResource.role(state.getUser().getName()));
|
||||
}
|
||||
catch (UnsupportedOperationException e)
|
||||
|
|
|
|||
|
|
@ -18,28 +18,31 @@
|
|||
|
||||
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;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.service.ClientWarn;
|
||||
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
|
||||
|
|
@ -64,36 +67,8 @@ public class PasswordGuardrail extends CustomGuardrail<String>
|
|||
GuardrailsDiagnostics.failed(name, redactedMsg);
|
||||
|
||||
if (state != null || throwOnNullClientState)
|
||||
throw new PasswordGuardrailException(message, redactedMessage);
|
||||
throwException(message, redactedMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
}
|
||||
protected abstract void throwException(String message, String redactedMessage);
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ import static java.lang.String.format;
|
|||
|
||||
public class CassandraPasswordConfiguration
|
||||
{
|
||||
public static final int MAX_LENGTH = 1000;
|
||||
|
||||
public static final int MAX_CHARACTERISTICS = 4;
|
||||
|
||||
// default values
|
||||
|
|
@ -173,6 +175,11 @@ public class CassandraPasswordConfiguration
|
|||
if (lengthWarn < 0) throw mustBePositiveException(LENGTH_WARN_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)
|
||||
throw getValidationException(MAX_LENGTH_KEY, maxLength, LENGTH_WARN_KEY, lengthWarn);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package org.apache.cassandra.db.guardrails;
|
|||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
|
|
@ -54,16 +55,13 @@ public class CassandraPasswordGenerator extends ValueGenerator<String>
|
|||
}
|
||||
|
||||
@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;
|
||||
|
||||
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 (!dictionaryAware || ((PasswordDictionaryAware<?>) validator).foundInDictionary(generatedPassword).isValid())
|
||||
|
|
@ -76,12 +74,6 @@ public class CassandraPasswordGenerator extends ValueGenerator<String>
|
|||
"Check your configuration and try again.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(ValueValidator<String> validator)
|
||||
{
|
||||
return generate(configuration.lengthWarn, validator);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CustomGuardrailConfig getParameters()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ public class CustomGuardrail<VALUE> extends 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.
|
||||
|
|
@ -84,7 +85,7 @@ public class CustomGuardrail<VALUE> extends Guardrail
|
|||
* @param value value to validate by the validator of this guardrail
|
||||
* @param state client's state
|
||||
*/
|
||||
public void guard(VALUE value, ClientState state)
|
||||
public void validate(VALUE value, ClientState state)
|
||||
{
|
||||
if (!enabled(state))
|
||||
return;
|
||||
|
|
@ -111,28 +112,37 @@ public class CustomGuardrail<VALUE> extends Guardrail
|
|||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
public VALUE generate(ClientState state)
|
||||
{
|
||||
return getGenerator().generate(size, getValidator());
|
||||
return generate(state, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a valid value.
|
||||
*
|
||||
* @param state client state
|
||||
* @param options options to use upon generation
|
||||
* @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)
|
||||
{
|
||||
|
||||
Map<String, Object> mergedMap = new HashMap<>(getValidator().getParameters());
|
||||
|
||||
if (newConfig != null)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
package org.apache.cassandra.db.guardrails;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -29,6 +28,7 @@ import com.google.common.annotations.VisibleForTesting;
|
|||
import com.google.common.collect.ImmutableSet;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||
import org.apache.cassandra.config.DataStorageSpec;
|
||||
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.service.ClientState;
|
||||
import org.apache.cassandra.service.disk.usage.DiskUsageBroadcaster;
|
||||
import org.apache.cassandra.utils.JsonUtils;
|
||||
import org.apache.cassandra.utils.MBeanWrapper;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
|
@ -556,8 +557,14 @@ public final class Guardrails implements GuardrailsMBean
|
|||
/**
|
||||
* Guardrail on passwords for CREATE / ALTER ROLE statements.
|
||||
*/
|
||||
public static final PasswordGuardrail password =
|
||||
new PasswordGuardrail(() -> CONFIG_PROVIDER.getOrCreate(null).getPasswordValidatorConfig());
|
||||
public static final PasswordPolicyGuardrail passwordPolicy =
|
||||
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
|
||||
{
|
||||
|
|
@ -1506,15 +1513,55 @@ public final class Guardrails implements GuardrailsMBean
|
|||
}
|
||||
|
||||
@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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -644,7 +644,12 @@ public interface GuardrailsConfig
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.apache.cassandra.db.guardrails;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
|
@ -1127,14 +1126,26 @@ public interface GuardrailsMBean
|
|||
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.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
package org.apache.cassandra.db.guardrails;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
|
|
@ -28,9 +29,9 @@ import org.apache.cassandra.exceptions.ConfigurationException;
|
|||
*/
|
||||
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)
|
||||
{
|
||||
|
|
@ -38,13 +39,7 @@ public class NoOpGenerator<VALUE> extends ValueGenerator<VALUE>
|
|||
}
|
||||
|
||||
@Override
|
||||
public VALUE generate(int size, ValueValidator<VALUE> validator)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VALUE generate(ValueValidator<VALUE> validator)
|
||||
public VALUE generate(ValueValidator<VALUE> validator, Map<String, Object> options)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
package org.apache.cassandra.db.guardrails;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
|
@ -30,7 +31,9 @@ import org.apache.cassandra.exceptions.ConfigurationException;
|
|||
*/
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
package org.apache.cassandra.db.guardrails;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -27,20 +29,16 @@ import org.apache.cassandra.exceptions.ConfigurationException;
|
|||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Map.of;
|
||||
|
||||
/**
|
||||
* Generates a value which respective {@link ValueValidator} successfuly validates.
|
||||
*/
|
||||
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";
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
* @param validator validator to validate generated value with
|
||||
* @return generated value of given size
|
||||
* @return generated value.
|
||||
*/
|
||||
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
|
||||
* @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
|
||||
|
|
@ -80,6 +103,17 @@ public abstract class ValueGenerator<VALUE>
|
|||
*/
|
||||
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}.
|
||||
*
|
||||
|
|
@ -100,7 +134,7 @@ public abstract class ValueGenerator<VALUE>
|
|||
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.",
|
||||
name);
|
||||
return (ValueGenerator<VALUE>) NO_OP_GENERATOR;
|
||||
return (ValueGenerator<VALUE>) NoOpGenerator.INSTANCE;
|
||||
}
|
||||
|
||||
if (!className.contains("."))
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import org.apache.cassandra.exceptions.ConfigurationException;
|
|||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
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.
|
||||
|
|
@ -41,10 +40,7 @@ public abstract class ValueValidator<VALUE>
|
|||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(ValueValidator.class);
|
||||
|
||||
public static final String CLASS_NAME_KEY = "class_name";
|
||||
|
||||
private static final ValueValidator<?> NO_OP_VALIDATOR =
|
||||
new NoOpValidator<>(new CustomGuardrailConfig(of(CLASS_NAME_KEY, NoOpValidator.class.getCanonicalName())));
|
||||
public static final String VALIDATOR_CLASS_NAME_KEY = "validator_class_name";
|
||||
|
||||
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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
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)
|
||||
{
|
||||
String className = config.resolveString(CLASS_NAME_KEY);
|
||||
String className = config.resolveString(VALIDATOR_CLASS_NAME_KEY);
|
||||
|
||||
if (className == null || className.isEmpty())
|
||||
{
|
||||
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);
|
||||
return (ValueValidator<VALUE>) NO_OP_VALIDATOR;
|
||||
"'validator_class_name' or its value is null or empty string. No-op validator will be used.", name);
|
||||
return (ValueValidator<VALUE>) NoOpValidator.INSTANCE;
|
||||
}
|
||||
|
||||
if (!className.contains("."))
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ public abstract class GuardrailsConfigCommand extends AbstractCommand
|
|||
"SimpleStrategyEnabled", "simplestrategy_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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,41 +18,39 @@
|
|||
|
||||
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 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.db.guardrails.UUIDRoleNameGenerator;
|
||||
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.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.fail;
|
||||
|
||||
public class GuardrailPasswordTest extends TestBaseImpl
|
||||
public class GuardrailsRoleNamePolicyTest extends TestBaseImpl
|
||||
{
|
||||
@Test
|
||||
public void testInvalidConfigurationPreventsNodeFromStart()
|
||||
{
|
||||
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||
config.put("class_name", CassandraPasswordValidator.class.getName());
|
||||
config.put("generator_class_name", CassandraPasswordGenerator.class.getName());
|
||||
config.put("illegal_sequence_length", -1);
|
||||
config.put("generator_class_name", UUIDRoleNameGenerator.class.getName());
|
||||
config.put("min_generated_name_size", 5);
|
||||
|
||||
try (Cluster ignored = build(1)
|
||||
.withConfig(c -> c.with(Feature.NETWORK, Feature.NATIVE_PROTOCOL, Feature.GOSSIP)
|
||||
.set("password_validator", config))
|
||||
.withConfig(c -> c.set("role_name_policy", config))
|
||||
.start())
|
||||
{
|
||||
fail("should throw ConfigurationException");
|
||||
|
|
@ -67,29 +65,23 @@ public class GuardrailPasswordTest extends TestBaseImpl
|
|||
public void testDisabledReconfiguration() throws Throwable
|
||||
{
|
||||
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||
config.put("class_name", CassandraPasswordValidator.class.getName());
|
||||
config.put("generator_class_name", CassandraPasswordGenerator.class.getName());
|
||||
config.put("generator_class_name", UUIDRoleNameGenerator.class.getName());
|
||||
|
||||
try (Cluster cluster = build(1)
|
||||
.withConfig(c -> c.with(Feature.JMX)
|
||||
.set("password_validator_reconfiguration_enabled", false)
|
||||
.set("password_validator", config))
|
||||
.withConfig(c -> c.with(JMX)
|
||||
.set("role_name_policy_reconfiguration_enabled", false)
|
||||
.set("role_name_policy", config))
|
||||
.start();
|
||||
JMXConnector connector = JMXUtil.getJmxConnector(cluster.get(1).config()))
|
||||
{
|
||||
MBeanServerConnection mbsc = connector.getMBeanServerConnection();
|
||||
|
||||
config.put("illegal_sequence_length", -1);
|
||||
|
||||
mbsc.invoke(ObjectName.getInstance(Guardrails.MBEAN_NAME),
|
||||
"reconfigurePasswordValidator",
|
||||
new Object[]{ config },
|
||||
new String[]{ Map.class.getName() });
|
||||
mbsc.setAttribute(ObjectName.getInstance(MBEAN_NAME), new Attribute("RoleNamePolicy", JSON_OBJECT_MAPPER.writeValueAsString(config)));
|
||||
|
||||
assertFalse(cluster.get(1)
|
||||
.logs()
|
||||
.watchFor("It is not possible to reconfigure password guardrail because " +
|
||||
"property 'password_validator_reconfiguration_enabled' is set to false.")
|
||||
.watchFor("It is not possible to reconfigure role_name_policy guardrail because " +
|
||||
"property 'role_name_policy_reconfiguration_enabled' is set to false.")
|
||||
.getResult()
|
||||
.isEmpty());
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +69,6 @@ public class CassandraPasswordGeneratorTest
|
|||
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||
|
||||
assertEquals(20, generator.generate(validator).length());
|
||||
assertEquals(30, generator.generate(30, validator).length());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -393,13 +393,17 @@ public class CassandraPasswordValidatorTest
|
|||
public void testMaxLength()
|
||||
{
|
||||
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||
config.put(CassandraPasswordConfiguration.LENGTH_WARN_KEY, 999);
|
||||
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||
CassandraPasswordGenerator generator = new CassandraPasswordGenerator(config);
|
||||
String password = generator.generate(1000, validator);
|
||||
assertEquals(1000, password.length());
|
||||
String password = generator.generate(validator);
|
||||
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)
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,17 +18,13 @@
|
|||
|
||||
package org.apache.cassandra.db.guardrails;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.exceptions.SyntaxException;
|
||||
import org.apache.cassandra.transport.Message;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
|
|
@ -36,41 +32,69 @@ import static java.lang.String.format;
|
|||
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_WARN_KEY;
|
||||
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
|
||||
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.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
|
||||
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
|
||||
public void testPasswordGuardrailForUser() throws Throwable
|
||||
{
|
||||
entity = "USER";
|
||||
testPasswordGuardrailInternal();
|
||||
// enable password guardrail
|
||||
setPasswordPolicy(getConfig(true));
|
||||
|
||||
// test that creation of a role with invalid password does not work anymore
|
||||
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
|
||||
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.",
|
||||
null, true);
|
||||
}
|
||||
|
|
@ -93,7 +117,7 @@ public class GuardrailPasswordTest extends GuardrailTester
|
|||
public void testGeneratedAndHashedPasswordCannotBeUsedTogether() throws Throwable
|
||||
{
|
||||
// enable password guardrail
|
||||
setGuardrail(getConfig(true));
|
||||
setPasswordPolicy(getConfig(true));
|
||||
|
||||
String hashedPassword = BCrypt.hashpw("doesnotmatter", BCrypt.gensalt(10));
|
||||
|
||||
|
|
@ -109,10 +133,10 @@ public class GuardrailPasswordTest extends GuardrailTester
|
|||
@Test
|
||||
public void testAllSpecialCharactersArePossibleToUseInCQLQuery()
|
||||
{
|
||||
setGuardrail(getConfig(true));
|
||||
setPasswordPolicy(getConfig(true));
|
||||
|
||||
PasswordGuardrail guardrail = new PasswordGuardrail(() -> new CustomGuardrailConfig(getConfig(true)));
|
||||
String userGeneratedPassword = guardrail.generate(20);
|
||||
PasswordPolicyGuardrail guardrail = new PasswordPolicyGuardrail(() -> new CustomGuardrailConfig(getConfig(true)));
|
||||
String userGeneratedPassword = guardrail.generate(userClientState, Map.of());
|
||||
String allSpecialCharacters = CassandraPasswordValidator.specialCharacters.getCharacters();
|
||||
String passwordWithAllSpecialChars = userGeneratedPassword + allSpecialCharacters;
|
||||
|
||||
|
|
@ -120,90 +144,6 @@ public class GuardrailPasswordTest extends GuardrailTester
|
|||
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,
|
||||
String roleName,
|
||||
String password,
|
||||
|
|
@ -212,23 +152,7 @@ public class GuardrailPasswordTest extends GuardrailTester
|
|||
String expectedRedactedMessage,
|
||||
boolean assertFails) throws Throwable
|
||||
{
|
||||
String query;
|
||||
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;
|
||||
final String queryToExecute = getQueryToExecute(create, roleName, password);
|
||||
if (assertFails)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (validate)
|
||||
{
|
||||
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(LENGTH_FAIL_KEY, 15);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
package org.apache.cassandra.db.guardrails;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import org.junit.Test;
|
||||
|
|
@ -47,7 +48,9 @@ public class ValueGeneratorTest
|
|||
assertNotNull(defaultBooleans);
|
||||
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);
|
||||
for (int i = 0; i < 20; i++)
|
||||
assertEquals(true, twentyBooleans[i]);
|
||||
|
|
@ -77,14 +80,6 @@ public class ValueGeneratorTest
|
|||
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
|
||||
public Boolean[] generate(ValueValidator<Boolean[]> validator)
|
||||
{
|
||||
|
|
@ -93,6 +88,12 @@ public class ValueGeneratorTest
|
|||
return booleans;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean[] generate(ValueValidator<Boolean[]> validator, Map<String, Object> options)
|
||||
{
|
||||
return generate(validator);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CustomGuardrailConfig getParameters()
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import org.apache.cassandra.exceptions.ConfigurationException;
|
|||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
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.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
|
@ -60,7 +60,7 @@ public class ValueValidatorTest
|
|||
public void testValidatorCreation()
|
||||
{
|
||||
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);
|
||||
|
||||
ValueValidator<Boolean> booleanValidator = ValueValidator.getValidator("boolean validator", config);
|
||||
|
|
@ -75,7 +75,7 @@ public class ValueValidatorTest
|
|||
public void testValidatorCreationWithInvalidConfiguration()
|
||||
{
|
||||
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))
|
||||
.isInstanceOf(ConfigurationException.class)
|
||||
|
|
|
|||
Loading…
Reference in New Issue