mirror of https://github.com/apache/cassandra
CEP-24 Password validation / generation
patch by Stefan Miklosovic; reviewed by Dinesh Joshi, Francisco Guerrero for CASSANDRA-17457
This commit is contained in:
parent
12c2c6d645
commit
d336dda112
|
|
@ -376,5 +376,9 @@
|
||||||
<groupId>com.vdurmont</groupId>
|
<groupId>com.vdurmont</groupId>
|
||||||
<artifactId>semver4j</artifactId>
|
<artifactId>semver4j</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.passay</groupId>
|
||||||
|
<artifactId>passay</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -1252,6 +1252,11 @@
|
||||||
<artifactId>semver4j</artifactId>
|
<artifactId>semver4j</artifactId>
|
||||||
<version>3.1.0</version>
|
<version>3.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.passay</groupId>
|
||||||
|
<artifactId>passay</artifactId>
|
||||||
|
<version>1.6.4</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
5.1
|
5.1
|
||||||
|
* CEP-24 Password validation / generation (CASSANDRA-17457)
|
||||||
* Reconfigure CMS after replacement, bootstrap and move operations (CASSANDRA-19705)
|
* Reconfigure CMS after replacement, bootstrap and move operations (CASSANDRA-19705)
|
||||||
* Support querying LocalStrategy tables with any partitioner (CASSANDRA-19692)
|
* Support querying LocalStrategy tables with any partitioner (CASSANDRA-19692)
|
||||||
* Relax slow_query_log_timeout for MultiNodeSAITest (CASSANDRA-19693)
|
* Relax slow_query_log_timeout for MultiNodeSAITest (CASSANDRA-19693)
|
||||||
|
|
|
||||||
4
NEWS.txt
4
NEWS.txt
|
|
@ -84,6 +84,10 @@ New features
|
||||||
- Authentication mode is exposed in system_views.clients table, nodetool clientstats and ClientMetrics
|
- Authentication mode is exposed in system_views.clients table, nodetool clientstats and ClientMetrics
|
||||||
to help operators identify which authentication modes are being used. nodetool clientstats introduces --verbose flag
|
to help operators identify which authentication modes are being used. nodetool clientstats introduces --verbose flag
|
||||||
behind which this information is visible.
|
behind which this information is visible.
|
||||||
|
- CEP-24 - Password validation / generation. When built-in 'password_validator' guardrail is enabled, it will
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
Upgrading
|
Upgrading
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ be sitting in front of a prompt:
|
||||||
|
|
||||||
----
|
----
|
||||||
Connected to Test Cluster at localhost:9160.
|
Connected to Test Cluster at localhost:9160.
|
||||||
[cqlsh 6.3.0 | Cassandra 5.0-SNAPSHOT | CQL spec 3.4.7 | Native protocol v5]
|
[cqlsh 6.3.0 | Cassandra 5.0-SNAPSHOT | CQL spec 3.4.8 | Native protocol v5]
|
||||||
Use HELP for help.
|
Use HELP for help.
|
||||||
cqlsh>
|
cqlsh>
|
||||||
----
|
----
|
||||||
|
|
|
||||||
|
|
@ -2148,6 +2148,67 @@ drop_compact_storage_enabled: false
|
||||||
# This would also apply to system keyspaces.
|
# This would also apply to system keyspaces.
|
||||||
# maximum_replication_factor_warn_threshold: -1
|
# maximum_replication_factor_warn_threshold: -1
|
||||||
# maximum_replication_factor_fail_threshold: -1
|
# maximum_replication_factor_fail_threshold: -1
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# # 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.
|
||||||
|
# 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.
|
||||||
|
# # 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
|
||||||
|
# # 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_validator_reconfiguration_enabled: true
|
||||||
|
|
||||||
# Guardrail to enable a CREATE or ALTER TABLE statement when default_time_to_live is set to 0
|
# Guardrail to enable a CREATE or ALTER TABLE statement when default_time_to_live is set to 0
|
||||||
# and the table is using TimeWindowCompactionStrategy compaction or a subclass of it.
|
# and the table is using TimeWindowCompactionStrategy compaction or a subclass of it.
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
#
|
#
|
||||||
-->
|
-->
|
||||||
|
|
||||||
h1. Cassandra Query Language (CQL) v3.4.7
|
h1. Cassandra Query Language (CQL) v3.4.8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1280,6 +1280,7 @@ bc(syntax)..
|
||||||
<create-role-stmt> ::= CREATE ROLE ( IF NOT EXISTS )? <identifier> ( WITH <option> ( AND <option> )* )?
|
<create-role-stmt> ::= CREATE ROLE ( IF NOT EXISTS )? <identifier> ( WITH <option> ( AND <option> )* )?
|
||||||
|
|
||||||
<option> ::= ("HASHED")? PASSWORD = <string>
|
<option> ::= ("HASHED")? PASSWORD = <string>
|
||||||
|
| GENERATED PASSWORD
|
||||||
| LOGIN = <boolean>
|
| LOGIN = <boolean>
|
||||||
| SUPERUSER = <boolean>
|
| SUPERUSER = <boolean>
|
||||||
| OPTIONS = <map_literal>
|
| OPTIONS = <map_literal>
|
||||||
|
|
@ -1298,6 +1299,7 @@ CREATE ROLE bob WITH PASSWORD = 'password_b' AND LOGIN = true AND SUPERUSER = tr
|
||||||
CREATE ROLE carlos WITH OPTIONS = { 'custom_option1' : 'option1_value', 'custom_option2' : 99 };
|
CREATE ROLE carlos WITH OPTIONS = { 'custom_option1' : 'option1_value', 'custom_option2' : 99 };
|
||||||
CREATE ROLE rob WITH LOGIN = true and PASSWORD = 'password_c' AND ACCESS FROM ALL CIDRS;
|
CREATE ROLE rob WITH LOGIN = true and PASSWORD = 'password_c' AND ACCESS FROM ALL CIDRS;
|
||||||
CREATE ROLE hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
|
CREATE ROLE hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
|
||||||
|
CREATE ROLE tom WITH LOGIN = true and GENERATED PASSWORD;
|
||||||
|
|
||||||
|
|
||||||
By default roles do not possess @LOGIN@ privileges or @SUPERUSER@ status.
|
By default roles do not possess @LOGIN@ privileges or @SUPERUSER@ status.
|
||||||
|
|
@ -1315,6 +1317,10 @@ h4(#createRolePwd). Setting credentials for internal authentication
|
||||||
Use the @WITH PASSWORD@ clause to set a password for internal authentication, enclosing the password in single quotation marks.
|
Use the @WITH PASSWORD@ clause to set a password for internal authentication, enclosing the password in single quotation marks.
|
||||||
If internal authentication has not been set up or the role does not have @LOGIN@ privileges, the @WITH PASSWORD@ clause is not necessary.
|
If internal authentication has not been set up or the role does not have @LOGIN@ privileges, the @WITH PASSWORD@ clause is not necessary.
|
||||||
|
|
||||||
|
When @WITH GENERATED PASSWORD@ is used, Cassandra provides out-of-the-box CassandraPasswordValidator and CassandraPasswordGenerator
|
||||||
|
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,
|
||||||
|
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.
|
||||||
|
|
||||||
h4(#createRoleConditional). Creating a role conditionally
|
h4(#createRoleConditional). Creating a role conditionally
|
||||||
|
|
||||||
Attempting to create an existing role results in an invalid query condition unless the @IF NOT EXISTS@ option is used. If the option is used and the role exists, the statement is a no-op.
|
Attempting to create an existing role results in an invalid query condition unless the @IF NOT EXISTS@ option is used. If the option is used and the role exists, the statement is a no-op.
|
||||||
|
|
@ -1330,7 +1336,8 @@ __Syntax:__
|
||||||
bc(syntax)..
|
bc(syntax)..
|
||||||
<alter-role-stmt> ::= ALTER ROLE (IF EXISTS)? <identifier> ( WITH <option> ( AND <option> )* )?
|
<alter-role-stmt> ::= ALTER ROLE (IF EXISTS)? <identifier> ( WITH <option> ( AND <option> )* )?
|
||||||
|
|
||||||
<option> ::= PASSWORD = <string>
|
<option> ::= ("HASHED")? PASSWORD = <string>
|
||||||
|
| GENERATED PASSWORD
|
||||||
| LOGIN = <boolean>
|
| LOGIN = <boolean>
|
||||||
| SUPERUSER = <boolean>
|
| SUPERUSER = <boolean>
|
||||||
| OPTIONS = <map_literal>
|
| OPTIONS = <map_literal>
|
||||||
|
|
@ -1343,9 +1350,10 @@ p.
|
||||||
__Sample:__
|
__Sample:__
|
||||||
|
|
||||||
bc(sample).
|
bc(sample).
|
||||||
ALTER ROLE bob WITH PASSWORD = 'PASSWORD_B' AND SUPERUSER = false;
|
ALTER ROLE IF EXISTS bob WITH PASSWORD = 'PASSWORD_B' AND SUPERUSER = false;
|
||||||
ALTER ROLE rob WITH LOGIN = true and PASSWORD = 'password_c' AND ACCESS FROM ALL CIDRS;
|
ALTER ROLE IF EXISTS rob WITH LOGIN = true and PASSWORD = 'password_c' AND ACCESS FROM ALL CIDRS;
|
||||||
ALTER ROLE hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
|
ALTER ROLE IF EXISTS hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
|
||||||
|
ALTER ROLE IF EXISTS hob WITH LOGIN = true and GENERATED PASSWORD;
|
||||||
|
|
||||||
Conditions on executing @ALTER ROLE@ statements:
|
Conditions on executing @ALTER ROLE@ statements:
|
||||||
|
|
||||||
|
|
@ -1458,10 +1466,12 @@ Prior to the introduction of roles in Cassandra 2.2, authentication and authoriz
|
||||||
|
|
||||||
__Syntax:__
|
__Syntax:__
|
||||||
|
|
||||||
bc(syntax)..
|
bc(syntax)..
|
||||||
<create-user-statement> ::= CREATE USER ( IF NOT EXISTS )? <identifier> ( WITH ("HASHED")? PASSWORD <string> )? (<option>)?
|
<create-user-statement> ::= CREATE USER ( IF NOT EXISTS )? <identifier> ( WITH <option> ( AND <option> )* )?
|
||||||
|
|
||||||
<option> ::= SUPERUSER
|
<option> ::= ("HASHED")? PASSWORD = <string>
|
||||||
|
| GENERATED PASSWORD
|
||||||
|
| SUPERUSER
|
||||||
| NOSUPERUSER
|
| NOSUPERUSER
|
||||||
p.
|
p.
|
||||||
|
|
||||||
|
|
@ -1470,6 +1480,7 @@ __Sample:__
|
||||||
bc(sample).
|
bc(sample).
|
||||||
CREATE USER alice WITH PASSWORD 'password_a' SUPERUSER;
|
CREATE USER alice WITH PASSWORD 'password_a' SUPERUSER;
|
||||||
CREATE USER bob WITH PASSWORD 'password_b' NOSUPERUSER;
|
CREATE USER bob WITH PASSWORD 'password_b' NOSUPERUSER;
|
||||||
|
CREATE USER tom WITH GENERATED PASSWORD;
|
||||||
|
|
||||||
@CREATE USER@ is equivalent to @CREATE ROLE@ where the @LOGIN@ option is @true@. So, the following pairs of statements are equivalent:
|
@CREATE USER@ is equivalent to @CREATE ROLE@ where the @LOGIN@ option is @true@. So, the following pairs of statements are equivalent:
|
||||||
|
|
||||||
|
|
@ -1495,9 +1506,11 @@ h3(#alterUserStmt). ALTER USER
|
||||||
__Syntax:__
|
__Syntax:__
|
||||||
|
|
||||||
bc(syntax)..
|
bc(syntax)..
|
||||||
<alter-user-statement> ::= ALTER USER (IF EXISTS)? <identifier> ( WITH PASSWORD <string> )? ( <option> )?
|
<alter-user-statement> ::= ALTER USER (IF EXISTS)? <identifier> ( WITH <option> ( AND <option> )* )?
|
||||||
|
|
||||||
<option> ::= SUPERUSER
|
<option> ::= ("HASHED")? PASSWORD = <string>
|
||||||
|
| GENERATED PASSWORD
|
||||||
|
| SUPERUSER
|
||||||
| NOSUPERUSER
|
| NOSUPERUSER
|
||||||
p.
|
p.
|
||||||
If the user does not exist, the statement will return an error, unless @IF EXISTS@ is used in which case the operation is a no-op.
|
If the user does not exist, the statement will return an error, unless @IF EXISTS@ is used in which case the operation is a no-op.
|
||||||
|
|
@ -2660,6 +2673,11 @@ h2(#changes). Changes
|
||||||
|
|
||||||
The following describes the changes in each version of CQL.
|
The following describes the changes in each version of CQL.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
h3. 3.4.7
|
h3. 3.4.7
|
||||||
|
|
||||||
* Remove deprecated functions @dateOf@ and @unixTimestampOf@, replaced by @to_timestamp@ and @to_unixtimestamp@ (see "CASSANDRA-18328":https://issues.apache.org/jira/browse/CASSANDRA-18328).
|
* Remove deprecated functions @dateOf@ and @unixTimestampOf@, replaced by @to_timestamp@ and @to_unixtimestamp@ (see "CASSANDRA-18328":https://issues.apache.org/jira/browse/CASSANDRA-18328).
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ The following describes the changes in each version of CQL.
|
||||||
== 3.4.8
|
== 3.4.8
|
||||||
|
|
||||||
* Add support for the BETWEEN operator in WHERE clauses (`19604`)
|
* Add support for the BETWEEN operator in WHERE clauses (`19604`)
|
||||||
|
* Add support for GENERATED PASSWORD clause (`17457`)
|
||||||
|
|
||||||
== 3.4.7
|
== 3.4.7
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1480,12 +1480,12 @@ syntax_rules += r'''
|
||||||
;
|
;
|
||||||
|
|
||||||
<createUserStatement> ::= "CREATE" "USER" ( "IF" "NOT" "EXISTS" )? <username>
|
<createUserStatement> ::= "CREATE" "USER" ( "IF" "NOT" "EXISTS" )? <username>
|
||||||
( "WITH" ("HASHED")? "PASSWORD" <stringLiteral> )?
|
( ("WITH" ("HASHED")? "PASSWORD" <stringLiteral>) | ("WITH" "GENERATED" "PASSWORD") )?
|
||||||
( "SUPERUSER" | "NOSUPERUSER" )?
|
( "SUPERUSER" | "NOSUPERUSER" )?
|
||||||
;
|
;
|
||||||
|
|
||||||
<alterUserStatement> ::= "ALTER" "USER" ("IF" "EXISTS")? <username>
|
<alterUserStatement> ::= "ALTER" "USER" ("IF" "EXISTS")? <username>
|
||||||
( "WITH" "PASSWORD" <stringLiteral> )?
|
( ("WITH" "PASSWORD" <stringLiteral>) | ("WITH" "GENERATED" "PASSWORD") )?
|
||||||
( "SUPERUSER" | "NOSUPERUSER" )?
|
( "SUPERUSER" | "NOSUPERUSER" )?
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -1511,6 +1511,7 @@ syntax_rules += r'''
|
||||||
;
|
;
|
||||||
|
|
||||||
<roleProperty> ::= (("HASHED")? "PASSWORD") "=" <stringLiteral>
|
<roleProperty> ::= (("HASHED")? "PASSWORD") "=" <stringLiteral>
|
||||||
|
| "GENERATED" "PASSWORD"
|
||||||
| "OPTIONS" "=" <mapLiteral>
|
| "OPTIONS" "=" <mapLiteral>
|
||||||
| "SUPERUSER" "=" <boolean>
|
| "SUPERUSER" "=" <boolean>
|
||||||
| "LOGIN" "=" <boolean>
|
| "LOGIN" "=" <boolean>
|
||||||
|
|
|
||||||
|
|
@ -541,7 +541,7 @@ class Shell(cmd.Cmd):
|
||||||
ksname = self.current_keyspace
|
ksname = self.current_keyspace
|
||||||
ksmeta = self.get_keyspace_meta(ksname)
|
ksmeta = self.get_keyspace_meta(ksname)
|
||||||
if tablename not in ksmeta.tables:
|
if tablename not in ksmeta.tables:
|
||||||
if ksname == 'system_auth' and tablename in ['roles', 'role_permissions']:
|
if ksname == 'system_auth' and tablename in ['roles', 'role_permissions', 'generated_password']:
|
||||||
self.get_fake_auth_table_meta(ksname, tablename)
|
self.get_fake_auth_table_meta(ksname, tablename)
|
||||||
else:
|
else:
|
||||||
raise ColumnFamilyNotFound("Column family {} not found".format(tablename))
|
raise ColumnFamilyNotFound("Column family {} not found".format(tablename))
|
||||||
|
|
@ -564,6 +564,10 @@ class Shell(cmd.Cmd):
|
||||||
table_meta.columns['role'] = ColumnMetadata(table_meta, 'role', cassandra.cqltypes.UTF8Type)
|
table_meta.columns['role'] = ColumnMetadata(table_meta, 'role', cassandra.cqltypes.UTF8Type)
|
||||||
table_meta.columns['resource'] = ColumnMetadata(table_meta, 'resource', cassandra.cqltypes.UTF8Type)
|
table_meta.columns['resource'] = ColumnMetadata(table_meta, 'resource', cassandra.cqltypes.UTF8Type)
|
||||||
table_meta.columns['permission'] = ColumnMetadata(table_meta, 'permission', cassandra.cqltypes.UTF8Type)
|
table_meta.columns['permission'] = ColumnMetadata(table_meta, 'permission', cassandra.cqltypes.UTF8Type)
|
||||||
|
elif tablename == 'generated_password':
|
||||||
|
ks_meta = KeyspaceMetadata(ksname, True, None, None)
|
||||||
|
table_meta = TableMetadata(ks_meta, 'generated_password')
|
||||||
|
table_meta.columns['generated_password'] = ColumnMetadata(table_meta, 'generated_password', cassandra.cqltypes.UTF8Type)
|
||||||
else:
|
else:
|
||||||
raise ColumnFamilyNotFound("Column family {} not found".format(tablename))
|
raise ColumnFamilyNotFound("Column family {} not found".format(tablename))
|
||||||
|
|
||||||
|
|
@ -932,6 +936,10 @@ class Shell(cmd.Cmd):
|
||||||
self.print_result(result, self.get_table_meta('system_auth', 'roles'))
|
self.print_result(result, self.get_table_meta('system_auth', 'roles'))
|
||||||
elif statement.query_string.lower().startswith("list"):
|
elif statement.query_string.lower().startswith("list"):
|
||||||
self.print_result(result, self.get_table_meta('system_auth', 'role_permissions'))
|
self.print_result(result, self.get_table_meta('system_auth', 'role_permissions'))
|
||||||
|
elif statement.query_string.lower().startswith("create user") or statement.query_string.lower().startswith("create role"):
|
||||||
|
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 result:
|
elif result:
|
||||||
# CAS INSERT/UPDATE
|
# CAS INSERT/UPDATE
|
||||||
self.writeresult("")
|
self.writeresult("")
|
||||||
|
|
|
||||||
|
|
@ -1022,7 +1022,8 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
||||||
self.trycompletions('ALTER TYPE ', choices=['IF', 'system_views.',
|
self.trycompletions('ALTER TYPE ', choices=['IF', 'system_views.',
|
||||||
'tags', 'system_traces.', 'system_distributed.', 'system_metrics.',
|
'tags', 'system_traces.', 'system_distributed.', 'system_metrics.',
|
||||||
'phone_number', 'quote_udt', 'band_info_type', 'address', 'system.', 'system_schema.',
|
'phone_number', 'quote_udt', 'band_info_type', 'address', 'system.', 'system_schema.',
|
||||||
'system_auth.', 'system_virtual_schema.', 'system_cluster_metadata.', self.cqlsh.keyspace + '.'
|
'system_auth.', 'system_virtual_schema.', 'system_cluster_metadata.',
|
||||||
|
self.cqlsh.keyspace + '.'
|
||||||
])
|
])
|
||||||
self.trycompletions('ALTER TYPE IF EXISTS new_type ADD ', choices=['<new_field_name>', 'IF'])
|
self.trycompletions('ALTER TYPE IF EXISTS new_type ADD ', choices=['<new_field_name>', 'IF'])
|
||||||
self.trycompletions('ALTER TYPE IF EXISTS new_type ADD IF NOT EXISTS ', choices=['<new_field_name>'])
|
self.trycompletions('ALTER TYPE IF EXISTS new_type ADD IF NOT EXISTS ', choices=['<new_field_name>'])
|
||||||
|
|
@ -1034,7 +1035,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
||||||
def test_complete_in_create_role(self):
|
def test_complete_in_create_role(self):
|
||||||
self.trycompletions('CREATE ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
|
self.trycompletions('CREATE ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
|
||||||
self.trycompletions('CREATE ROLE IF ', immediate='NOT EXISTS ')
|
self.trycompletions('CREATE ROLE IF ', immediate='NOT EXISTS ')
|
||||||
self.trycompletions('CREATE ROLE foo WITH ', choices=['ACCESS', 'HASHED', 'LOGIN', 'OPTIONS', 'PASSWORD', 'SUPERUSER'])
|
self.trycompletions('CREATE ROLE foo WITH ', choices=['ACCESS', 'HASHED', 'LOGIN', 'OPTIONS', 'PASSWORD', 'SUPERUSER', 'GENERATED'])
|
||||||
self.trycompletions('CREATE ROLE foo WITH HASHED ', immediate='PASSWORD = ')
|
self.trycompletions('CREATE ROLE foo WITH HASHED ', immediate='PASSWORD = ')
|
||||||
self.trycompletions('CREATE ROLE foo WITH ACCESS TO ', choices=['ALL', 'DATACENTERS'])
|
self.trycompletions('CREATE ROLE foo WITH ACCESS TO ', choices=['ALL', 'DATACENTERS'])
|
||||||
self.trycompletions('CREATE ROLE foo WITH ACCESS TO ALL ', immediate='DATACENTERS ')
|
self.trycompletions('CREATE ROLE foo WITH ACCESS TO ALL ', immediate='DATACENTERS ')
|
||||||
|
|
@ -1044,7 +1045,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
||||||
def test_complete_in_alter_role(self):
|
def test_complete_in_alter_role(self):
|
||||||
self.trycompletions('ALTER ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
|
self.trycompletions('ALTER ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
|
||||||
self.trycompletions('ALTER ROLE foo ', immediate='WITH ')
|
self.trycompletions('ALTER ROLE foo ', immediate='WITH ')
|
||||||
self.trycompletions('ALTER ROLE foo WITH ', choices=['ACCESS', 'HASHED', 'LOGIN', 'OPTIONS', 'PASSWORD', 'SUPERUSER'])
|
self.trycompletions('ALTER ROLE foo WITH ', choices=['ACCESS', 'HASHED', 'LOGIN', 'OPTIONS', 'PASSWORD', 'SUPERUSER', 'GENERATED'])
|
||||||
self.trycompletions('ALTER ROLE foo WITH ACCESS TO ', choices=['ALL', 'DATACENTERS'])
|
self.trycompletions('ALTER ROLE foo WITH ACCESS TO ', choices=['ALL', 'DATACENTERS'])
|
||||||
self.trycompletions('ALTER ROLE foo WITH ACCESS FROM ', choices=['ALL', 'CIDRS'])
|
self.trycompletions('ALTER ROLE foo WITH ACCESS FROM ', choices=['ALL', 'CIDRS'])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ K_ROLES: R O L E S;
|
||||||
K_SUPERUSERS: S U P E R U S E R S;
|
K_SUPERUSERS: S U P E R U S E R S;
|
||||||
K_SUPERUSER: S U P E R U S E R;
|
K_SUPERUSER: S U P E R U S E R;
|
||||||
K_NOSUPERUSER: N O S U P E R U S E R;
|
K_NOSUPERUSER: N O S U P E R U S E R;
|
||||||
|
K_GENERATED: G E N E R A T E D;
|
||||||
K_PASSWORD: P A S S W O R D;
|
K_PASSWORD: P A S S W O R D;
|
||||||
K_HASHED: H A S H E D;
|
K_HASHED: H A S H E D;
|
||||||
K_LOGIN: L O G I N;
|
K_LOGIN: L O G I N;
|
||||||
|
|
|
||||||
|
|
@ -1197,6 +1197,14 @@ createUserStatement returns [CreateRoleStatement stmt]
|
||||||
{
|
{
|
||||||
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
||||||
}
|
}
|
||||||
|
if (opts.getPassword().isPresent() && opts.isGeneratedPassword())
|
||||||
|
{
|
||||||
|
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); }
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -1218,6 +1226,14 @@ alterUserStatement returns [AlterRoleStatement stmt]
|
||||||
{
|
{
|
||||||
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
||||||
}
|
}
|
||||||
|
if (opts.getPassword().isPresent() && opts.isGeneratedPassword())
|
||||||
|
{
|
||||||
|
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 AlterRoleStatement(name, opts, null, null, ifExists);
|
$stmt = new AlterRoleStatement(name, opts, null, null, ifExists);
|
||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
@ -1298,6 +1314,14 @@ createRoleStatement returns [CreateRoleStatement stmt]
|
||||||
{
|
{
|
||||||
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
||||||
}
|
}
|
||||||
|
if (opts.getPassword().isPresent() && opts.isGeneratedPassword())
|
||||||
|
{
|
||||||
|
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, dcperms.build(), cidrperms.build(), ifNotExists);
|
$stmt = new CreateRoleStatement(name, opts, dcperms.build(), cidrperms.build(), ifNotExists);
|
||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
@ -1329,6 +1353,14 @@ alterRoleStatement returns [AlterRoleStatement stmt]
|
||||||
{
|
{
|
||||||
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
|
||||||
}
|
}
|
||||||
|
if (opts.getPassword().isPresent() && opts.isGeneratedPassword())
|
||||||
|
{
|
||||||
|
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 AlterRoleStatement(name, opts, dcperms.isModified() ? dcperms.build() : null, cidrperms.isModified() ? cidrperms.build() : null, ifExists);
|
$stmt = new AlterRoleStatement(name, opts, dcperms.isModified() ? dcperms.build() : null, cidrperms.isModified() ? cidrperms.build() : null, ifExists);
|
||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
@ -1373,6 +1405,7 @@ roleOptions[RoleOptions opts, DCPermissions.Builder dcperms, CIDRPermissions.Bui
|
||||||
|
|
||||||
roleOption[RoleOptions opts, DCPermissions.Builder dcperms, CIDRPermissions.Builder cidrperms]
|
roleOption[RoleOptions opts, DCPermissions.Builder dcperms, CIDRPermissions.Builder cidrperms]
|
||||||
: K_PASSWORD '=' v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
|
: K_PASSWORD '=' v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
|
||||||
|
| K_GENERATED K_PASSWORD { opts.setOption(IRoleManager.Option.GENERATED_PASSWORD, Boolean.TRUE); }
|
||||||
| K_HASHED K_PASSWORD '=' v=STRING_LITERAL { opts.setOption(IRoleManager.Option.HASHED_PASSWORD, $v.text); }
|
| K_HASHED K_PASSWORD '=' v=STRING_LITERAL { opts.setOption(IRoleManager.Option.HASHED_PASSWORD, $v.text); }
|
||||||
| K_OPTIONS '=' m=fullMapLiteral { opts.setOption(IRoleManager.Option.OPTIONS, convertPropertyMap(m)); }
|
| K_OPTIONS '=' m=fullMapLiteral { opts.setOption(IRoleManager.Option.OPTIONS, convertPropertyMap(m)); }
|
||||||
| K_SUPERUSER '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.SUPERUSER, Boolean.valueOf($b.text)); }
|
| K_SUPERUSER '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.SUPERUSER, Boolean.valueOf($b.text)); }
|
||||||
|
|
@ -1395,6 +1428,7 @@ cidrPermission[CIDRPermissions.Builder builder]
|
||||||
userPassword[RoleOptions opts]
|
userPassword[RoleOptions opts]
|
||||||
: K_PASSWORD v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
|
: K_PASSWORD v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
|
||||||
| K_HASHED K_PASSWORD v=STRING_LITERAL { opts.setOption(IRoleManager.Option.HASHED_PASSWORD, $v.text); }
|
| K_HASHED K_PASSWORD v=STRING_LITERAL { opts.setOption(IRoleManager.Option.HASHED_PASSWORD, $v.text); }
|
||||||
|
| K_GENERATED K_PASSWORD { opts.setOption(IRoleManager.Option.GENERATED_PASSWORD, Boolean.TRUE); }
|
||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1991,6 +2025,7 @@ basic_unreserved_keyword returns [String str]
|
||||||
| K_NOLOGIN
|
| K_NOLOGIN
|
||||||
| K_OPTIONS
|
| K_OPTIONS
|
||||||
| K_PASSWORD
|
| K_PASSWORD
|
||||||
|
| K_GENERATED
|
||||||
| K_HASHED
|
| K_HASHED
|
||||||
| K_EXISTS
|
| K_EXISTS
|
||||||
| K_CUSTOM
|
| K_CUSTOM
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import org.apache.cassandra.cql3.PasswordObfuscator;
|
||||||
import org.apache.cassandra.cql3.QueryEvents;
|
import org.apache.cassandra.cql3.QueryEvents;
|
||||||
import org.apache.cassandra.cql3.QueryOptions;
|
import org.apache.cassandra.cql3.QueryOptions;
|
||||||
import org.apache.cassandra.cql3.statements.BatchStatement;
|
import org.apache.cassandra.cql3.statements.BatchStatement;
|
||||||
|
import org.apache.cassandra.db.guardrails.PasswordGuardrail;
|
||||||
import org.apache.cassandra.exceptions.AuthenticationException;
|
import org.apache.cassandra.exceptions.AuthenticationException;
|
||||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
import org.apache.cassandra.exceptions.PreparedQueryNotFoundException;
|
import org.apache.cassandra.exceptions.PreparedQueryNotFoundException;
|
||||||
|
|
@ -388,6 +389,10 @@ public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listene
|
||||||
return "Syntax Exception. Obscured for security reasons.";
|
return "Syntax Exception. Obscured for security reasons.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (e instanceof PasswordGuardrail.PasswordGuardrailException)
|
||||||
|
{
|
||||||
|
return ((PasswordGuardrail.PasswordGuardrailException) e).redactedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
return PasswordObfuscator.obfuscate(e.getMessage());
|
return PasswordObfuscator.obfuscate(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,10 +152,10 @@ public class CassandraRoleManager implements IRoleManager
|
||||||
public CassandraRoleManager()
|
public CassandraRoleManager()
|
||||||
{
|
{
|
||||||
supportedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
|
supportedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
|
||||||
? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.HASHED_PASSWORD)
|
? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD)
|
||||||
: ImmutableSet.of(Option.LOGIN, Option.SUPERUSER);
|
: ImmutableSet.of(Option.LOGIN, Option.SUPERUSER);
|
||||||
alterableOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
|
alterableOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
|
||||||
? ImmutableSet.of(Option.PASSWORD, Option.HASHED_PASSWORD)
|
? ImmutableSet.of(Option.PASSWORD, Option.HASHED_PASSWORD, Option.GENERATED_PASSWORD)
|
||||||
: ImmutableSet.<Option>of();
|
: ImmutableSet.<Option>of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ public interface IRoleManager extends AuthCache.BulkLoader<RoleResource, Set<Rol
|
||||||
*/
|
*/
|
||||||
public enum Option
|
public enum Option
|
||||||
{
|
{
|
||||||
SUPERUSER, PASSWORD, LOGIN, OPTIONS, HASHED_PASSWORD
|
SUPERUSER, PASSWORD, LOGIN, OPTIONS, HASHED_PASSWORD, GENERATED_PASSWORD
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,11 @@ public class RoleOptions
|
||||||
return Optional.ofNullable((String)options.get(IRoleManager.Option.PASSWORD));
|
return Optional.ofNullable((String)options.get(IRoleManager.Option.PASSWORD));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isGeneratedPassword()
|
||||||
|
{
|
||||||
|
return (Boolean) options.getOrDefault(IRoleManager.Option.GENERATED_PASSWORD, Boolean.FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the string value of the hashed password option.
|
* Return the string value of the hashed password option.
|
||||||
* @return hashed password option value
|
* @return hashed password option value
|
||||||
|
|
@ -164,6 +169,14 @@ public class RoleOptions
|
||||||
throw new InvalidRequestException("Invalid hashed password value. Please use jBcrypt.");
|
throw new InvalidRequestException("Invalid hashed password value. Please use jBcrypt.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case GENERATED_PASSWORD:
|
||||||
|
if (options.containsKey(IRoleManager.Option.PASSWORD))
|
||||||
|
throw new InvalidRequestException(String.format("Properties '%s' and '%s' are mutually exclusive",
|
||||||
|
IRoleManager.Option.PASSWORD, IRoleManager.Option.GENERATED_PASSWORD));
|
||||||
|
if (options.containsKey(IRoleManager.Option.HASHED_PASSWORD))
|
||||||
|
throw new InvalidRequestException(String.format("Properties '%s' and '%s' are mutually exclusive",
|
||||||
|
IRoleManager.Option.HASHED_PASSWORD, IRoleManager.Option.GENERATED_PASSWORD));
|
||||||
|
break;
|
||||||
case OPTIONS:
|
case OPTIONS:
|
||||||
if (!(option.getValue() instanceof Map))
|
if (!(option.getValue() instanceof Map))
|
||||||
throw new InvalidRequestException(String.format("Invalid value for property '%s'. " +
|
throw new InvalidRequestException(String.format("Invalid value for property '%s'. " +
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,10 @@ public enum CassandraRelevantProperties
|
||||||
CASSANDRA_MAX_HINT_TTL("cassandra.maxHintTTL", convertToString(Integer.MAX_VALUE)),
|
CASSANDRA_MAX_HINT_TTL("cassandra.maxHintTTL", convertToString(Integer.MAX_VALUE)),
|
||||||
CASSANDRA_MINIMUM_REPLICATION_FACTOR("cassandra.minimum_replication_factor"),
|
CASSANDRA_MINIMUM_REPLICATION_FACTOR("cassandra.minimum_replication_factor"),
|
||||||
CASSANDRA_NETTY_USE_HEAP_ALLOCATOR("cassandra.netty_use_heap_allocator"),
|
CASSANDRA_NETTY_USE_HEAP_ALLOCATOR("cassandra.netty_use_heap_allocator"),
|
||||||
|
/**
|
||||||
|
* Number of attempts to generate a valid password before giving up.
|
||||||
|
*/
|
||||||
|
CASSANDRA_PASSWORD_GENERATOR_ATTEMTPS("cassandra.password.generator.attempts", "100"),
|
||||||
CASSANDRA_PID_FILE("cassandra-pidfile"),
|
CASSANDRA_PID_FILE("cassandra-pidfile"),
|
||||||
CASSANDRA_RACKDC_PROPERTIES("cassandra-rackdc.properties"),
|
CASSANDRA_RACKDC_PROPERTIES("cassandra-rackdc.properties"),
|
||||||
CASSANDRA_SKIP_AUTOMATIC_UDT_FIX("cassandra.skipautomaticudtfix"),
|
CASSANDRA_SKIP_AUTOMATIC_UDT_FIX("cassandra.skipautomaticudtfix"),
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import org.apache.cassandra.audit.AuditLogOptions;
|
import org.apache.cassandra.audit.AuditLogOptions;
|
||||||
import org.apache.cassandra.db.ConsistencyLevel;
|
import org.apache.cassandra.db.ConsistencyLevel;
|
||||||
|
import org.apache.cassandra.db.guardrails.CustomGuardrailConfig;
|
||||||
import org.apache.cassandra.fql.FullQueryLoggerOptions;
|
import org.apache.cassandra.fql.FullQueryLoggerOptions;
|
||||||
import org.apache.cassandra.index.internal.CassandraIndex;
|
import org.apache.cassandra.index.internal.CassandraIndex;
|
||||||
import org.apache.cassandra.io.compress.BufferType;
|
import org.apache.cassandra.io.compress.BufferType;
|
||||||
|
|
@ -940,6 +941,9 @@ public class Config
|
||||||
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_warn_threshold = null;
|
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_warn_threshold = null;
|
||||||
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_fail_threshold = null;
|
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_fail_threshold = null;
|
||||||
|
|
||||||
|
public volatile boolean password_validator_reconfiguration_enabled = true;
|
||||||
|
public volatile CustomGuardrailConfig password_validator = new CustomGuardrailConfig();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The variants of paxos implementation and semantics supported by Cassandra.
|
* The variants of paxos implementation and semantics supported by Cassandra.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -5255,4 +5255,9 @@ public class DatabaseDescriptor
|
||||||
{
|
{
|
||||||
return conf.triggers_policy;
|
return conf.triggers_policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isPasswordValidatorReconfigurationEnabled()
|
||||||
|
{
|
||||||
|
return conf.password_validator_reconfiguration_enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,11 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import org.apache.cassandra.cql3.statements.schema.TableAttributes;
|
import org.apache.cassandra.cql3.statements.schema.TableAttributes;
|
||||||
import org.apache.cassandra.db.ConsistencyLevel;
|
import org.apache.cassandra.db.ConsistencyLevel;
|
||||||
|
import org.apache.cassandra.db.guardrails.CustomGuardrailConfig;
|
||||||
import org.apache.cassandra.db.guardrails.Guardrails;
|
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||||
import org.apache.cassandra.db.guardrails.GuardrailsConfig;
|
import org.apache.cassandra.db.guardrails.GuardrailsConfig;
|
||||||
|
import org.apache.cassandra.db.guardrails.ValueGenerator;
|
||||||
|
import org.apache.cassandra.db.guardrails.ValueValidator;
|
||||||
import org.apache.cassandra.io.util.FileUtils;
|
import org.apache.cassandra.io.util.FileUtils;
|
||||||
import org.apache.cassandra.service.disk.usage.DiskUsageMonitor;
|
import org.apache.cassandra.service.disk.usage.DiskUsageMonitor;
|
||||||
|
|
||||||
|
|
@ -93,6 +96,7 @@ public class GuardrailsOptions implements GuardrailsConfig
|
||||||
config.sai_sstable_indexes_per_query_fail_threshold,
|
config.sai_sstable_indexes_per_query_fail_threshold,
|
||||||
"sai_sstable_indexes_per_query",
|
"sai_sstable_indexes_per_query",
|
||||||
false);
|
false);
|
||||||
|
validatePasswordValidator(config.password_validator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -817,6 +821,12 @@ public class GuardrailsOptions implements GuardrailsConfig
|
||||||
return config.maximum_replication_factor_fail_threshold;
|
return config.maximum_replication_factor_fail_threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CustomGuardrailConfig getPasswordValidatorConfig()
|
||||||
|
{
|
||||||
|
return config.password_validator;
|
||||||
|
}
|
||||||
|
|
||||||
public void setMaximumReplicationFactorThreshold(int warn, int fail)
|
public void setMaximumReplicationFactorThreshold(int warn, int fail)
|
||||||
{
|
{
|
||||||
validateMaxRFThreshold(warn, fail);
|
validateMaxRFThreshold(warn, fail);
|
||||||
|
|
@ -890,7 +900,7 @@ public class GuardrailsOptions implements GuardrailsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DurationSpec.LongMicrosecondsBound getMaximumTimestampWarnThreshold()
|
public DurationSpec.LongMicrosecondsBound getMaximumTimestampWarnThreshold()
|
||||||
{
|
{
|
||||||
return config.maximum_timestamp_warn_threshold;
|
return config.maximum_timestamp_warn_threshold;
|
||||||
}
|
}
|
||||||
|
|
@ -919,7 +929,7 @@ public class GuardrailsOptions implements GuardrailsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DurationSpec.LongMicrosecondsBound getMinimumTimestampWarnThreshold()
|
public DurationSpec.LongMicrosecondsBound getMinimumTimestampWarnThreshold()
|
||||||
{
|
{
|
||||||
return config.minimum_timestamp_warn_threshold;
|
return config.minimum_timestamp_warn_threshold;
|
||||||
}
|
}
|
||||||
|
|
@ -1258,4 +1268,17 @@ public class GuardrailsOptions implements GuardrailsConfig
|
||||||
"%s specified, but only %s are actually available on disk",
|
"%s specified, but only %s are actually available on disk",
|
||||||
maxDiskSize, FileUtils.stringifyFileSize(diskSize)));
|
maxDiskSize, FileUtils.stringifyFileSize(diskSize)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method tests not only valid configuration, but also that what we generate with
|
||||||
|
* a generator passes its validator, so we avoid the situation when a configuration would be valid according
|
||||||
|
* to a concrete implementation of a password validator but passwords it would generate would not pass
|
||||||
|
* its validator which is clearly not desired.
|
||||||
|
*
|
||||||
|
* @param config configuration to use for generator and validator
|
||||||
|
*/
|
||||||
|
private static void validatePasswordValidator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
ValueGenerator.getGenerator("password", config).generate(ValueValidator.getValidator("password", config));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,22 @@ package org.apache.cassandra.cql3.statements;
|
||||||
|
|
||||||
import org.apache.cassandra.audit.AuditLogContext;
|
import org.apache.cassandra.audit.AuditLogContext;
|
||||||
import org.apache.cassandra.audit.AuditLogEntryType;
|
import org.apache.cassandra.audit.AuditLogEntryType;
|
||||||
import org.apache.cassandra.auth.*;
|
import org.apache.cassandra.auth.AuthenticatedUser;
|
||||||
|
import org.apache.cassandra.auth.CIDRPermissions;
|
||||||
|
import org.apache.cassandra.auth.DCPermissions;
|
||||||
|
import org.apache.cassandra.auth.IRoleManager;
|
||||||
import org.apache.cassandra.auth.IRoleManager.Option;
|
import org.apache.cassandra.auth.IRoleManager.Option;
|
||||||
|
import org.apache.cassandra.auth.Permission;
|
||||||
|
import org.apache.cassandra.auth.RoleOptions;
|
||||||
|
import org.apache.cassandra.auth.RoleResource;
|
||||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.cql3.PasswordObfuscator;
|
import org.apache.cassandra.cql3.PasswordObfuscator;
|
||||||
import org.apache.cassandra.cql3.RoleName;
|
import org.apache.cassandra.cql3.RoleName;
|
||||||
import org.apache.cassandra.exceptions.*;
|
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||||
|
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||||
|
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||||
|
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||||
|
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||||
import org.apache.cassandra.service.ClientState;
|
import org.apache.cassandra.service.ClientState;
|
||||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||||
|
|
@ -72,7 +82,7 @@ public class AlterRoleStatement extends AuthenticationStatement
|
||||||
cidrPermissions.validate();
|
cidrPermissions.validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate login here before authorize to avoid leaking user existence to anonymous users.
|
// validate login here before authorize, to avoid leaking user existence to anonymous users.
|
||||||
state.ensureNotAnonymous();
|
state.ensureNotAnonymous();
|
||||||
if (!DatabaseDescriptor.getRoleManager().isExistingRole(role))
|
if (!DatabaseDescriptor.getRoleManager().isExistingRole(role))
|
||||||
{
|
{
|
||||||
|
|
@ -114,6 +124,19 @@ public class AlterRoleStatement extends AuthenticationStatement
|
||||||
|
|
||||||
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
|
||||||
{
|
{
|
||||||
|
if (opts.isGeneratedPassword())
|
||||||
|
{
|
||||||
|
String generatedPassword = Guardrails.password.generate();
|
||||||
|
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 " +
|
||||||
|
"in cassandra.yaml to be able to generate passwords.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.getPassword().isPresent())
|
||||||
|
Guardrails.password.guard(opts.getPassword().get(), state);
|
||||||
|
|
||||||
if (!opts.isEmpty())
|
if (!opts.isEmpty())
|
||||||
DatabaseDescriptor.getRoleManager().alterRole(state.getUser(), role, opts);
|
DatabaseDescriptor.getRoleManager().alterRole(state.getUser(), role, opts);
|
||||||
|
|
||||||
|
|
@ -123,7 +146,7 @@ public class AlterRoleStatement extends AuthenticationStatement
|
||||||
if (cidrPermissions != null)
|
if (cidrPermissions != null)
|
||||||
DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions);
|
DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions);
|
||||||
|
|
||||||
return null;
|
return getResultMessage(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -17,20 +17,36 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.cassandra.cql3.statements;
|
package org.apache.cassandra.cql3.statements;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.cassandra.auth.Permission;
|
import org.apache.cassandra.auth.Permission;
|
||||||
|
import org.apache.cassandra.auth.RoleOptions;
|
||||||
import org.apache.cassandra.auth.RoleResource;
|
import org.apache.cassandra.auth.RoleResource;
|
||||||
import org.apache.cassandra.cql3.CQLStatement;
|
import org.apache.cassandra.cql3.CQLStatement;
|
||||||
|
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||||
|
import org.apache.cassandra.cql3.ColumnSpecification;
|
||||||
import org.apache.cassandra.cql3.QueryOptions;
|
import org.apache.cassandra.cql3.QueryOptions;
|
||||||
|
import org.apache.cassandra.cql3.ResultSet;
|
||||||
|
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||||
import org.apache.cassandra.exceptions.RequestExecutionException;
|
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||||
import org.apache.cassandra.exceptions.RequestValidationException;
|
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||||
|
import org.apache.cassandra.schema.SchemaConstants;
|
||||||
import org.apache.cassandra.service.ClientState;
|
import org.apache.cassandra.service.ClientState;
|
||||||
import org.apache.cassandra.service.QueryState;
|
import org.apache.cassandra.service.QueryState;
|
||||||
import org.apache.cassandra.transport.Dispatcher;
|
import org.apache.cassandra.transport.Dispatcher;
|
||||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
|
||||||
|
|
||||||
public abstract class AuthenticationStatement extends CQLStatement.Raw implements CQLStatement
|
public abstract class AuthenticationStatement extends CQLStatement.Raw implements CQLStatement
|
||||||
{
|
{
|
||||||
|
private static final List<ColumnSpecification> GENERATED_PASSWORD_METADATA =
|
||||||
|
List.of(new ColumnSpecification(SchemaConstants.AUTH_KEYSPACE_NAME,
|
||||||
|
"generated_password",
|
||||||
|
new ColumnIdentifier("generated_password", true),
|
||||||
|
UTF8Type.instance));
|
||||||
|
|
||||||
public AuthenticationStatement prepare(ClientState state)
|
public AuthenticationStatement prepare(ClientState state)
|
||||||
{
|
{
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -71,5 +87,21 @@ public abstract class AuthenticationStatement extends CQLStatement.Raw implement
|
||||||
{
|
{
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected ResultMessage getResultMessage(RoleOptions opts)
|
||||||
|
{
|
||||||
|
if (!opts.isGeneratedPassword())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (opts.getPassword().isEmpty())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
ResultSet.ResultMetadata resultMetadata = new ResultSet.ResultMetadata(GENERATED_PASSWORD_METADATA);
|
||||||
|
ResultSet result = new ResultSet(resultMetadata);
|
||||||
|
|
||||||
|
result.addColumnValue(bytes(opts.getPassword().get()));
|
||||||
|
|
||||||
|
return new ResultMessage.Rows(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,25 @@ package org.apache.cassandra.cql3.statements;
|
||||||
|
|
||||||
import org.apache.cassandra.audit.AuditLogContext;
|
import org.apache.cassandra.audit.AuditLogContext;
|
||||||
import org.apache.cassandra.audit.AuditLogEntryType;
|
import org.apache.cassandra.audit.AuditLogEntryType;
|
||||||
import org.apache.cassandra.auth.*;
|
|
||||||
|
import org.apache.cassandra.auth.AuthenticatedUser;
|
||||||
|
import org.apache.cassandra.auth.CIDRPermissions;
|
||||||
|
import org.apache.cassandra.auth.DCPermissions;
|
||||||
|
import org.apache.cassandra.auth.IRoleManager;
|
||||||
|
import org.apache.cassandra.auth.Permission;
|
||||||
|
import org.apache.cassandra.auth.RoleOptions;
|
||||||
|
import org.apache.cassandra.auth.RoleResource;
|
||||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.cql3.PasswordObfuscator;
|
import org.apache.cassandra.cql3.PasswordObfuscator;
|
||||||
import org.apache.cassandra.cql3.RoleName;
|
import org.apache.cassandra.cql3.RoleName;
|
||||||
import org.apache.cassandra.exceptions.*;
|
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||||
|
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||||
|
import org.apache.cassandra.exceptions.RequestExecutionException;
|
||||||
|
import org.apache.cassandra.exceptions.RequestValidationException;
|
||||||
|
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||||
import org.apache.cassandra.service.ClientState;
|
import org.apache.cassandra.service.ClientState;
|
||||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||||
|
|
||||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||||
|
|
||||||
|
|
@ -87,6 +99,18 @@ public class CreateRoleStatement extends AuthenticationStatement
|
||||||
if (ifNotExists && DatabaseDescriptor.getRoleManager().isExistingRole(role))
|
if (ifNotExists && DatabaseDescriptor.getRoleManager().isExistingRole(role))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
if (opts.isGeneratedPassword())
|
||||||
|
{
|
||||||
|
String generatedPassword = Guardrails.password.generate();
|
||||||
|
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 " +
|
||||||
|
"in cassandra.yaml to be able to generate passwords.");
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.getPassword().ifPresent(password -> Guardrails.password.guard(password, state));
|
||||||
|
|
||||||
DatabaseDescriptor.getRoleManager().createRole(state.getUser(), role, opts);
|
DatabaseDescriptor.getRoleManager().createRole(state.getUser(), role, opts);
|
||||||
if (DatabaseDescriptor.getNetworkAuthorizer().requireAuthorization())
|
if (DatabaseDescriptor.getNetworkAuthorizer().requireAuthorization())
|
||||||
{
|
{
|
||||||
|
|
@ -98,14 +122,14 @@ public class CreateRoleStatement extends AuthenticationStatement
|
||||||
|
|
||||||
grantPermissionsToCreator(state);
|
grantPermissionsToCreator(state);
|
||||||
|
|
||||||
return null;
|
return getResultMessage(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grant all applicable permissions on the newly created role to the user performing the request
|
* Grant all applicable permissions on the newly created role to the user performing the request
|
||||||
* see also: AlterTableStatement#createdResources() and the overridden implementations
|
* see also: AlterTableStatement#createdResources() and the overridden implementations
|
||||||
* of it in subclasses CreateKeyspaceStatement & CreateTableStatement.
|
* of it in subclasses CreateKeyspaceStatement & CreateTableStatement.
|
||||||
* @param state
|
* @param state client state
|
||||||
*/
|
*/
|
||||||
private void grantPermissionsToCreator(ClientState state)
|
private void grantPermissionsToCreator(ClientState state)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
/*
|
||||||
|
* 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.Arrays;
|
||||||
|
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
import org.passay.IllegalSequenceRule;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
public class CassandraPasswordConfiguration
|
||||||
|
{
|
||||||
|
public static final int MAX_CHARACTERISTICS = 4;
|
||||||
|
|
||||||
|
// default values
|
||||||
|
public static final int DEFAULT_CHARACTERISTIC_WARN = 3;
|
||||||
|
public static final int DEFAULT_CHARACTERISTIC_FAIL = 2;
|
||||||
|
|
||||||
|
public static final int DEFAULT_MAX_LENGTH = 1000;
|
||||||
|
public static final int DEFAULT_LENGTH_WARN = 12;
|
||||||
|
public static final int DEFAULT_LENGTH_FAIL = 8;
|
||||||
|
|
||||||
|
public static final int DEFAULT_UPPER_CASE_WARN = 2;
|
||||||
|
public static final int DEFAULT_UPPER_CASE_FAIL = 1;
|
||||||
|
|
||||||
|
public static final int DEFAULT_LOWER_CASE_WARN = 2;
|
||||||
|
public static final int DEFAULT_LOWER_CASE_FAIL = 1;
|
||||||
|
|
||||||
|
public static final int DEFAULT_DIGIT_WARN = 2;
|
||||||
|
public static final int DEFAULT_DIGIT_FAIL = 1;
|
||||||
|
|
||||||
|
public static final int DEFAULT_SPECIAL_WARN = 2;
|
||||||
|
public static final int DEFAULT_SPECIAL_FAIL = 1;
|
||||||
|
|
||||||
|
public static final int DEFAULT_ILLEGAL_SEQUENCE_LENGTH = 5;
|
||||||
|
|
||||||
|
public static final String CHARACTERISTIC_WARN_KEY = "characteristic_warn";
|
||||||
|
public static final String CHARACTERISTIC_FAIL_KEY = "characteristic_fail";
|
||||||
|
|
||||||
|
public static final String MAX_LENGTH_KEY = "max_length";
|
||||||
|
public static final String LENGTH_WARN_KEY = "length_warn";
|
||||||
|
public static final String LENGTH_FAIL_KEY = "length_fail";
|
||||||
|
|
||||||
|
public static final String UPPER_CASE_WARN_KEY = "upper_case_warn";
|
||||||
|
public static final String UPPER_CASE_FAIL_KEY = "upper_case_fail";
|
||||||
|
|
||||||
|
public static final String LOWER_CASE_WARN_KEY = "lower_case_warn";
|
||||||
|
public static final String LOWER_CASE_FAIL_KEY = "lower_case_fail";
|
||||||
|
|
||||||
|
public static final String DIGIT_WARN_KEY = "digit_warn";
|
||||||
|
public static final String DIGIT_FAIL_KEY = "digit_fail";
|
||||||
|
|
||||||
|
public static final String SPECIAL_WARN_KEY = "special_warn";
|
||||||
|
public static final String SPECIAL_FAIL_KEY = "special_fail";
|
||||||
|
|
||||||
|
public static final String ILLEGAL_SEQUENCE_LENGTH_KEY = "illegal_sequence_length";
|
||||||
|
|
||||||
|
public static final String DETAILED_MESSAGES_KEY = "detailed_messages";
|
||||||
|
|
||||||
|
protected final int characteristicsWarn;
|
||||||
|
protected final int characteristicsFail;
|
||||||
|
|
||||||
|
protected final int maxLength;
|
||||||
|
protected final int lengthWarn;
|
||||||
|
protected final int lengthFail;
|
||||||
|
|
||||||
|
protected final int upperCaseWarn;
|
||||||
|
protected final int upperCaseFail;
|
||||||
|
|
||||||
|
protected final int lowerCaseWarn;
|
||||||
|
protected final int lowerCaseFail;
|
||||||
|
|
||||||
|
protected final int digitsWarn;
|
||||||
|
protected final int digitsFail;
|
||||||
|
|
||||||
|
protected final int specialsWarn;
|
||||||
|
protected final int specialsFail;
|
||||||
|
|
||||||
|
protected final int illegalSequenceLength;
|
||||||
|
|
||||||
|
public boolean detailedMessages;
|
||||||
|
|
||||||
|
public CustomGuardrailConfig asCustomGuardrailConfig()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(CHARACTERISTIC_WARN_KEY, characteristicsWarn);
|
||||||
|
config.put(CHARACTERISTIC_FAIL_KEY, characteristicsFail);
|
||||||
|
config.put(MAX_LENGTH_KEY, maxLength);
|
||||||
|
config.put(LENGTH_WARN_KEY, lengthWarn);
|
||||||
|
config.put(LENGTH_FAIL_KEY, lengthFail);
|
||||||
|
config.put(UPPER_CASE_WARN_KEY, upperCaseWarn);
|
||||||
|
config.put(UPPER_CASE_FAIL_KEY, upperCaseFail);
|
||||||
|
config.put(LOWER_CASE_WARN_KEY, lowerCaseWarn);
|
||||||
|
config.put(LOWER_CASE_FAIL_KEY, lowerCaseFail);
|
||||||
|
config.put(DIGIT_WARN_KEY, digitsWarn);
|
||||||
|
config.put(DIGIT_FAIL_KEY, digitsFail);
|
||||||
|
config.put(SPECIAL_WARN_KEY, specialsWarn);
|
||||||
|
config.put(SPECIAL_FAIL_KEY, specialsFail);
|
||||||
|
config.put(ILLEGAL_SEQUENCE_LENGTH_KEY, illegalSequenceLength);
|
||||||
|
config.put(DETAILED_MESSAGES_KEY, detailedMessages);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CassandraPasswordConfiguration(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
characteristicsWarn = config.resolveInteger(CHARACTERISTIC_WARN_KEY, DEFAULT_CHARACTERISTIC_WARN);
|
||||||
|
characteristicsFail = config.resolveInteger(CHARACTERISTIC_FAIL_KEY, DEFAULT_CHARACTERISTIC_FAIL);
|
||||||
|
|
||||||
|
maxLength = config.resolveInteger(MAX_LENGTH_KEY, DEFAULT_MAX_LENGTH);
|
||||||
|
lengthWarn = config.resolveInteger(LENGTH_WARN_KEY, DEFAULT_LENGTH_WARN);
|
||||||
|
lengthFail = config.resolveInteger(LENGTH_FAIL_KEY, DEFAULT_LENGTH_FAIL);
|
||||||
|
|
||||||
|
upperCaseWarn = config.resolveInteger(UPPER_CASE_WARN_KEY, DEFAULT_UPPER_CASE_WARN);
|
||||||
|
upperCaseFail = config.resolveInteger(UPPER_CASE_FAIL_KEY, DEFAULT_UPPER_CASE_FAIL);
|
||||||
|
|
||||||
|
lowerCaseWarn = config.resolveInteger(LOWER_CASE_WARN_KEY, DEFAULT_LOWER_CASE_WARN);
|
||||||
|
lowerCaseFail = config.resolveInteger(LOWER_CASE_FAIL_KEY, DEFAULT_LOWER_CASE_FAIL);
|
||||||
|
|
||||||
|
digitsWarn = config.resolveInteger(DIGIT_WARN_KEY, DEFAULT_DIGIT_WARN);
|
||||||
|
digitsFail = config.resolveInteger(DIGIT_FAIL_KEY, DEFAULT_DIGIT_FAIL);
|
||||||
|
|
||||||
|
specialsWarn = config.resolveInteger(SPECIAL_WARN_KEY, DEFAULT_SPECIAL_WARN);
|
||||||
|
specialsFail = config.resolveInteger(SPECIAL_FAIL_KEY, DEFAULT_SPECIAL_FAIL);
|
||||||
|
|
||||||
|
illegalSequenceLength = config.resolveInteger(ILLEGAL_SEQUENCE_LENGTH_KEY, DEFAULT_ILLEGAL_SEQUENCE_LENGTH);
|
||||||
|
detailedMessages = config.resolveBoolean(DETAILED_MESSAGES_KEY, true);
|
||||||
|
|
||||||
|
validateParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationException mustBePositiveException(String parameter)
|
||||||
|
{
|
||||||
|
throw new ConfigurationException(parameter + " must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateParameters() throws ConfigurationException
|
||||||
|
{
|
||||||
|
if (maxLength < 0) throw mustBePositiveException(MAX_LENGTH_KEY);
|
||||||
|
if (characteristicsWarn < 0) throw mustBePositiveException(CHARACTERISTIC_WARN_KEY);
|
||||||
|
if (characteristicsFail < 0) throw mustBePositiveException(CHARACTERISTIC_FAIL_KEY);
|
||||||
|
if (lowerCaseWarn < 0) throw mustBePositiveException(LOWER_CASE_WARN_KEY);
|
||||||
|
if (lowerCaseFail < 0) throw mustBePositiveException(LOWER_CASE_FAIL_KEY);
|
||||||
|
if (upperCaseWarn < 0) throw mustBePositiveException(UPPER_CASE_WARN_KEY);
|
||||||
|
if (upperCaseFail < 0) throw mustBePositiveException(UPPER_CASE_FAIL_KEY);
|
||||||
|
if (specialsWarn < 0) throw mustBePositiveException(SPECIAL_WARN_KEY);
|
||||||
|
if (specialsFail < 0) throw mustBePositiveException(SPECIAL_FAIL_KEY);
|
||||||
|
if (digitsWarn < 0) throw mustBePositiveException(DIGIT_WARN_KEY);
|
||||||
|
if (digitsFail < 0) throw mustBePositiveException(DIGIT_FAIL_KEY);
|
||||||
|
if (lengthWarn < 0) throw mustBePositiveException(LENGTH_WARN_KEY);
|
||||||
|
if (lengthFail < 0) throw mustBePositiveException(LENGTH_FAIL_KEY);
|
||||||
|
|
||||||
|
if (maxLength <= lengthWarn)
|
||||||
|
throw getValidationException(MAX_LENGTH_KEY, maxLength, LENGTH_WARN_KEY, lengthWarn);
|
||||||
|
|
||||||
|
if (lengthWarn <= lengthFail)
|
||||||
|
throw getValidationException(LENGTH_WARN_KEY, lengthWarn, LENGTH_FAIL_KEY, lengthFail);
|
||||||
|
|
||||||
|
if (specialsWarn <= specialsFail)
|
||||||
|
throw getValidationException(SPECIAL_WARN_KEY,
|
||||||
|
specialsWarn,
|
||||||
|
SPECIAL_FAIL_KEY,
|
||||||
|
specialsFail);
|
||||||
|
|
||||||
|
if (digitsWarn <= digitsFail)
|
||||||
|
throw getValidationException(DIGIT_WARN_KEY,
|
||||||
|
digitsWarn,
|
||||||
|
DIGIT_FAIL_KEY,
|
||||||
|
digitsFail);
|
||||||
|
|
||||||
|
if (upperCaseWarn <= upperCaseFail)
|
||||||
|
throw getValidationException(UPPER_CASE_WARN_KEY,
|
||||||
|
upperCaseWarn,
|
||||||
|
UPPER_CASE_FAIL_KEY,
|
||||||
|
upperCaseFail);
|
||||||
|
|
||||||
|
if (lowerCaseWarn <= lowerCaseFail)
|
||||||
|
throw getValidationException(LOWER_CASE_WARN_KEY,
|
||||||
|
lowerCaseWarn,
|
||||||
|
LOWER_CASE_FAIL_KEY,
|
||||||
|
lowerCaseFail);
|
||||||
|
|
||||||
|
if (illegalSequenceLength < IllegalSequenceRule.MINIMUM_SEQUENCE_LENGTH)
|
||||||
|
throw new ConfigurationException(format("Illegal sequence length can not be lower than %s.",
|
||||||
|
IllegalSequenceRule.MINIMUM_SEQUENCE_LENGTH));
|
||||||
|
|
||||||
|
if (characteristicsWarn > 4)
|
||||||
|
throw new ConfigurationException(format("%s can not be bigger than %s",
|
||||||
|
CHARACTERISTIC_WARN_KEY,
|
||||||
|
MAX_CHARACTERISTICS));
|
||||||
|
|
||||||
|
if (characteristicsFail > 4)
|
||||||
|
throw new ConfigurationException(format("%s can not be bigger than %s",
|
||||||
|
CHARACTERISTIC_FAIL_KEY,
|
||||||
|
MAX_CHARACTERISTICS));
|
||||||
|
|
||||||
|
if (characteristicsFail == characteristicsWarn)
|
||||||
|
throw new ConfigurationException(format("%s can not be equal to %s. You set %s and %s respectively.",
|
||||||
|
CHARACTERISTIC_FAIL_KEY,
|
||||||
|
CHARACTERISTIC_WARN_KEY,
|
||||||
|
characteristicsFail,
|
||||||
|
characteristicsWarn));
|
||||||
|
|
||||||
|
if (characteristicsFail > characteristicsWarn)
|
||||||
|
throw new ConfigurationException(format("%s can not be bigger than %s. You have set %s and %s respectively.",
|
||||||
|
CHARACTERISTIC_FAIL_KEY,
|
||||||
|
CHARACTERISTIC_WARN_KEY,
|
||||||
|
characteristicsFail,
|
||||||
|
characteristicsWarn));
|
||||||
|
|
||||||
|
int[] minimumLengthsWarn = new int[]{ specialsWarn, digitsWarn,
|
||||||
|
upperCaseWarn, lowerCaseWarn };
|
||||||
|
Arrays.sort(minimumLengthsWarn);
|
||||||
|
|
||||||
|
int minimumLenghtOfWarnCharacteristics = 0;
|
||||||
|
for (int i = 0; i < characteristicsWarn; i++)
|
||||||
|
minimumLenghtOfWarnCharacteristics += minimumLengthsWarn[i];
|
||||||
|
|
||||||
|
if (minimumLenghtOfWarnCharacteristics > lengthWarn)
|
||||||
|
throw new ConfigurationException(format("The shortest password to pass the warning validator for any %s " +
|
||||||
|
"characteristics out of %s is %s but you have set the %s to %s.",
|
||||||
|
characteristicsWarn,
|
||||||
|
MAX_CHARACTERISTICS,
|
||||||
|
minimumLenghtOfWarnCharacteristics,
|
||||||
|
LENGTH_WARN_KEY,
|
||||||
|
lengthWarn));
|
||||||
|
|
||||||
|
int[] minimumLengthsFail = new int[]{ specialsFail, digitsFail,
|
||||||
|
upperCaseFail, lowerCaseFail };
|
||||||
|
Arrays.sort(minimumLengthsFail);
|
||||||
|
|
||||||
|
int minimumLenghtOfFailCharacteristics = 0;
|
||||||
|
for (int i = 0; i < characteristicsFail; i++)
|
||||||
|
minimumLenghtOfFailCharacteristics += minimumLengthsFail[i];
|
||||||
|
|
||||||
|
if (minimumLenghtOfFailCharacteristics > lengthFail)
|
||||||
|
throw new ConfigurationException(format("The shortest password to pass the failing validator for any %s " +
|
||||||
|
"characteristics out of %s is %s but you have set the %s to %s.",
|
||||||
|
characteristicsFail,
|
||||||
|
MAX_CHARACTERISTICS,
|
||||||
|
minimumLenghtOfFailCharacteristics,
|
||||||
|
LENGTH_FAIL_KEY,
|
||||||
|
lengthFail));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigurationException getValidationException(String key1, int value1, String key2, int value2)
|
||||||
|
{
|
||||||
|
return new ConfigurationException(format("%s of value %s is less or equal to %s of value %s",
|
||||||
|
key1, value1,
|
||||||
|
key2, value2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
import org.passay.CharacterRule;
|
||||||
|
import org.passay.EnglishCharacterData;
|
||||||
|
import org.passay.PasswordGenerator;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_PASSWORD_GENERATOR_ATTEMTPS;
|
||||||
|
import static org.passay.EnglishCharacterData.Digit;
|
||||||
|
|
||||||
|
public class CassandraPasswordGenerator extends ValueGenerator<String>
|
||||||
|
{
|
||||||
|
private final PasswordGenerator passwordGenerator;
|
||||||
|
private final List<CharacterRule> characterRules;
|
||||||
|
|
||||||
|
protected final CassandraPasswordConfiguration configuration;
|
||||||
|
private final int maxPasswordGenerationAttempts;
|
||||||
|
|
||||||
|
public CassandraPasswordGenerator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
configuration = new CassandraPasswordConfiguration(config);
|
||||||
|
characterRules = getCharacterGenerationRules(configuration.upperCaseWarn,
|
||||||
|
configuration.lowerCaseWarn,
|
||||||
|
configuration.digitsWarn,
|
||||||
|
configuration.specialsWarn);
|
||||||
|
|
||||||
|
int maxAttempts = CASSANDRA_PASSWORD_GENERATOR_ATTEMTPS.getInt();
|
||||||
|
this.maxPasswordGenerationAttempts = Math.max(maxAttempts, 1);
|
||||||
|
|
||||||
|
passwordGenerator = new PasswordGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generate(int size, ValueValidator<String> validator)
|
||||||
|
{
|
||||||
|
if (size > configuration.maxLength)
|
||||||
|
throw new ConfigurationException("Unable to generate a password of length " + size);
|
||||||
|
|
||||||
|
for (int i = 0; i < maxPasswordGenerationAttempts; i++)
|
||||||
|
{
|
||||||
|
String generatedPassword = passwordGenerator.generatePassword(size, characterRules);
|
||||||
|
if (validator.shouldWarn(generatedPassword, false).isEmpty())
|
||||||
|
return generatedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ConfigurationException("It was not possible to generate a valid password " +
|
||||||
|
"in " + maxPasswordGenerationAttempts + " attempts. " +
|
||||||
|
"Check your configuration and try again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generate(ValueValidator<String> validator)
|
||||||
|
{
|
||||||
|
return generate(configuration.lengthWarn, validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CustomGuardrailConfig getParameters()
|
||||||
|
{
|
||||||
|
return configuration.asCustomGuardrailConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateParameters() throws ConfigurationException
|
||||||
|
{
|
||||||
|
configuration.validateParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<CharacterRule> getCharacterGenerationRules(int upper, int lower, int digits, int special)
|
||||||
|
{
|
||||||
|
return Arrays.asList(new CharacterRule(EnglishCharacterData.UpperCase, upper),
|
||||||
|
new CharacterRule(EnglishCharacterData.LowerCase, lower),
|
||||||
|
new CharacterRule(Digit, digits),
|
||||||
|
new CharacterRule(CassandraPasswordValidator.specialCharacters, special));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,439 @@
|
||||||
|
/*
|
||||||
|
* 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.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
import org.passay.CharacterCharacteristicsRule;
|
||||||
|
import org.passay.CharacterData;
|
||||||
|
import org.passay.CharacterRule;
|
||||||
|
import org.passay.CyrillicCharacterData;
|
||||||
|
import org.passay.CyrillicModernCharacterData;
|
||||||
|
import org.passay.CyrillicModernSequenceData;
|
||||||
|
import org.passay.CyrillicSequenceData;
|
||||||
|
import org.passay.CzechCharacterData;
|
||||||
|
import org.passay.CzechSequenceData;
|
||||||
|
import org.passay.EnglishCharacterData;
|
||||||
|
import org.passay.EnglishSequenceData;
|
||||||
|
import org.passay.GermanCharacterData;
|
||||||
|
import org.passay.GermanSequenceData;
|
||||||
|
import org.passay.IllegalSequenceRule;
|
||||||
|
import org.passay.LengthRule;
|
||||||
|
import org.passay.PasswordData;
|
||||||
|
import org.passay.PasswordValidator;
|
||||||
|
import org.passay.PolishCharacterData;
|
||||||
|
import org.passay.PolishSequenceData;
|
||||||
|
import org.passay.Rule;
|
||||||
|
import org.passay.RuleResult;
|
||||||
|
import org.passay.RuleResultDetail;
|
||||||
|
import org.passay.RuleResultMetadata;
|
||||||
|
import org.passay.SequenceData;
|
||||||
|
import org.passay.WhitespaceRule;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static org.passay.EnglishCharacterData.Digit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The figures for each respective password characteristic and rule holds for the default configuration.
|
||||||
|
* <p>
|
||||||
|
* This password validator acts around 4 password characteristics which are:
|
||||||
|
* <ul>
|
||||||
|
* <li>minimum number of lower-case characters (at least 2 to not emit warning, at least 1 to not emit failure)</li>
|
||||||
|
* <li>minimum number of upper-case characters (at least 2 to not emit warning, at least 1 to not emit failure)</li>
|
||||||
|
* <li>minimum number of special characters (at least 2 to not emit warning, at least 1 to not emit failure)</li>
|
||||||
|
* <li>minimum number of digits (at least 2 to not emit warning, at least 1 to not emit failure)</li>
|
||||||
|
* </ul>
|
||||||
|
* To not emit warning, all characteristics have to be satisfied. A warning is emitted when 3 out of 4 are satistifed,
|
||||||
|
* and it emits a failure when 2 (or less) out of 4 are satisfied.
|
||||||
|
* <p>
|
||||||
|
* Additionally, this validator uses these password rules which can not be violated in order to have a chance
|
||||||
|
* to consider a password to be valid (in connection with above characteristics)
|
||||||
|
* <ul>
|
||||||
|
* <li>a password has to be at least 12 characters long to not emit a warning and at least 8 characters
|
||||||
|
* long to not emit a failure</li>
|
||||||
|
* <li>a password can not contain whitespace characters</li>
|
||||||
|
* <li>a password can not contain illegal sequences</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The default illegal-sequence length is 5 and its allowed minimum is 3.
|
||||||
|
* <p>
|
||||||
|
* Only some characters are contributing to the password characteristics above when it comes to
|
||||||
|
* upper-cased and lower-cased ones. All other (unicode) characters do not contribute to these characteristics,
|
||||||
|
* but they do contribute to the length rule.
|
||||||
|
* <p>
|
||||||
|
* For example, let's have this password: {@code 山13g.^Tg$棚Sd}. This password satisfies all 4 password characteristics
|
||||||
|
* above:
|
||||||
|
* <ul>
|
||||||
|
* <li>2 specials (contains 3)</li>
|
||||||
|
* <li>2 upper chars (contains 2)</li>
|
||||||
|
* <li>2 lower chars (contains 3)</li>
|
||||||
|
* <li>2 digits (contains 2)</li>
|
||||||
|
* </ul>
|
||||||
|
* Together 10 characters. That would not be enough but there are two more: {@code 山} and {@code 棚}
|
||||||
|
* which is together 12. Not all (unicode) characters are possible to be categorized to upper-case and lower-case
|
||||||
|
* categories so these characters do not contribute to these password characteristics but only to the length rule.
|
||||||
|
* <p>
|
||||||
|
* The characters which do contribute to lower-case and upper-case characteristics are:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link EnglishCharacterData}</li>
|
||||||
|
* <li>{@link CyrillicCharacterData}</li>
|
||||||
|
* <li>{@link CyrillicModernCharacterData}</li>
|
||||||
|
* <li>{@link GermanCharacterData}</li>
|
||||||
|
* <li>{@link PolishCharacterData}</li>
|
||||||
|
* <li>{@link CzechCharacterData}</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The characters which do contribute to {@link IllegalSequenceRule} rule are:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link EnglishSequenceData#Alphabetical}</li>
|
||||||
|
* <li>{@link EnglishSequenceData#Numerical}</li>
|
||||||
|
* <li>{@link EnglishSequenceData#USQwerty}</li>
|
||||||
|
* <li>{@link CyrillicSequenceData#Alphabetical}</li>
|
||||||
|
* <li>{@link CyrillicModernSequenceData#Alphabetical}</li>
|
||||||
|
* <li>{@link CzechSequenceData#Alphabetical}</li>
|
||||||
|
* <li>{@link GermanSequenceData#Alphabetical}</li>
|
||||||
|
* <li>{@link PolishSequenceData#Alphabetical}</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
* Passwords consisting of all characters belonging to unsupported character sets will be rejected and such
|
||||||
|
* passwords will not be validated. For example, this password will be rejected: {@code 卡桑德拉-卡桑德拉山山羊棚}.
|
||||||
|
* <p>
|
||||||
|
* The password validator acts on two levels when used in a guardrail, executed in this order:
|
||||||
|
* <ol>
|
||||||
|
* <li>tests if a password does not violate a failure threshold, if it does, failure is emitted and query failed</li>
|
||||||
|
* <li>tests if a password does not violate a warning threshold, if it does, warning is emitted</li>
|
||||||
|
* </ol>
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @see CharacterCharacteristicsRule
|
||||||
|
* @see CassandraPasswordConfiguration
|
||||||
|
* @see WhitespaceRule
|
||||||
|
* @see LengthRule
|
||||||
|
* @see IllegalSequenceRule
|
||||||
|
* @see ValueValidator#shouldWarn(Object, boolean)
|
||||||
|
* @see ValueValidator#shouldFail(Object, boolean)
|
||||||
|
*/
|
||||||
|
public class CassandraPasswordValidator extends ValueValidator<String>
|
||||||
|
{
|
||||||
|
protected final PasswordValidator warnValidator;
|
||||||
|
protected final PasswordValidator failValidator;
|
||||||
|
|
||||||
|
protected final CassandraPasswordConfiguration configuration;
|
||||||
|
private final UnsupportedCharsetRule unsupportedCharsetRule = new UnsupportedCharsetRule();
|
||||||
|
private final boolean provideDetailedMessages;
|
||||||
|
|
||||||
|
public CassandraPasswordValidator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
configuration = new CassandraPasswordConfiguration(config);
|
||||||
|
provideDetailedMessages = configuration.detailedMessages;
|
||||||
|
|
||||||
|
warnValidator = new PasswordValidator(getRules(configuration.lengthWarn,
|
||||||
|
configuration.maxLength,
|
||||||
|
configuration.characteristicsWarn,
|
||||||
|
configuration.illegalSequenceLength,
|
||||||
|
getCharacterValidationRules(configuration.upperCaseWarn,
|
||||||
|
configuration.lowerCaseWarn,
|
||||||
|
configuration.digitsWarn,
|
||||||
|
configuration.specialsWarn)));
|
||||||
|
failValidator = new PasswordValidator(getRules(configuration.lengthFail,
|
||||||
|
configuration.maxLength,
|
||||||
|
configuration.characteristicsFail,
|
||||||
|
configuration.illegalSequenceLength,
|
||||||
|
getCharacterValidationRules(configuration.upperCaseFail,
|
||||||
|
configuration.lowerCaseFail,
|
||||||
|
configuration.digitsFail,
|
||||||
|
configuration.specialsFail)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CustomGuardrailConfig getParameters()
|
||||||
|
{
|
||||||
|
return configuration.asCustomGuardrailConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ValidationViolation> shouldWarn(String password, boolean calledBySuperuser)
|
||||||
|
{
|
||||||
|
return executeValidation(warnValidator, password, calledBySuperuser, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ValidationViolation> shouldFail(String password, boolean calledBySuperUser)
|
||||||
|
{
|
||||||
|
return executeValidation(failValidator, password, calledBySuperUser, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ValidationViolation> executeValidation(PasswordValidator validator,
|
||||||
|
String passwordToValidate,
|
||||||
|
boolean calledBySuperUser,
|
||||||
|
boolean toWarn)
|
||||||
|
{
|
||||||
|
PasswordData passwordData = new PasswordData(passwordToValidate);
|
||||||
|
if (!unsupportedCharsetRule.validate(passwordData).isValid())
|
||||||
|
{
|
||||||
|
String message = (calledBySuperUser || provideDetailedMessages) ? "Unsupported language / character set for password validator"
|
||||||
|
: "Password complexity policy not met.";
|
||||||
|
return Optional.of(new ValidationViolation(message, UnsupportedCharsetRule.ERROR_CODE));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RuleResult result = validator.validate(passwordData);
|
||||||
|
return result.isValid() ? empty() : Optional.of(getValidationMessage(calledBySuperUser, validator, toWarn, result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateParameters() throws ConfigurationException
|
||||||
|
{
|
||||||
|
configuration.validateParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<CharacterRule> getCharacterValidationRules(int upper, int lower, int digits, int special)
|
||||||
|
{
|
||||||
|
return Arrays.asList(new CharacterRule(new CustomUpperCaseCharacterData(), upper),
|
||||||
|
new CharacterRule(new CustomLowerCaseCharacterData(), lower),
|
||||||
|
new CharacterRule(Digit, digits),
|
||||||
|
new CharacterRule(specialCharacters, special));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<Rule> getRules(int minLength,
|
||||||
|
int maxLength,
|
||||||
|
int characteristics,
|
||||||
|
int illegalSequenceLength,
|
||||||
|
List<CharacterRule> characterRules)
|
||||||
|
{
|
||||||
|
List<Rule> rules = new ArrayList<>();
|
||||||
|
|
||||||
|
rules.add(new LengthRule(minLength, maxLength));
|
||||||
|
|
||||||
|
CharacterCharacteristicsRule characteristicsRule = new CharacterCharacteristicsRule();
|
||||||
|
characteristicsRule.setNumberOfCharacteristics(characteristics + 1);
|
||||||
|
characteristicsRule.getRules().addAll(characterRules);
|
||||||
|
rules.add(characteristicsRule);
|
||||||
|
|
||||||
|
rules.add(new WhitespaceRule());
|
||||||
|
|
||||||
|
for (SequenceData sequenceData : getSequenceData())
|
||||||
|
rules.add(new IllegalSequenceRule(sequenceData, illegalSequenceLength, false));
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationViolation getValidationMessage(boolean calledBySuperuser,
|
||||||
|
PasswordValidator validator,
|
||||||
|
boolean toWarn,
|
||||||
|
RuleResult result)
|
||||||
|
{
|
||||||
|
Set<String> errorCodes = new HashSet<>();
|
||||||
|
for (RuleResultDetail ruleResultDetail : result.getDetails())
|
||||||
|
errorCodes.add(ruleResultDetail.getErrorCode());
|
||||||
|
|
||||||
|
String redactedMessage = errorCodes.toString();
|
||||||
|
|
||||||
|
if (calledBySuperuser || provideDetailedMessages)
|
||||||
|
{
|
||||||
|
String type = toWarn ? "warning" : "error";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Password was")
|
||||||
|
.append(toWarn ? " set, however it might not be strong enough according to the " +
|
||||||
|
"configured password strength policy. "
|
||||||
|
: " not set as it violated configured password strength policy. ")
|
||||||
|
.append("To fix this ")
|
||||||
|
.append(type)
|
||||||
|
.append(", the following has to be resolved: ");
|
||||||
|
|
||||||
|
for (String message : validator.getMessages(result))
|
||||||
|
sb.append(message).append(' ');
|
||||||
|
|
||||||
|
sb.append("You may also use 'GENERATED PASSWORD' upon role creation or alteration.");
|
||||||
|
|
||||||
|
String message = sb.toString();
|
||||||
|
|
||||||
|
return new ValidationViolation(message, redactedMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (toWarn)
|
||||||
|
{
|
||||||
|
return new ValidationViolation("Password was set, however it might not be strong enough " +
|
||||||
|
"according to the configured password strength policy.",
|
||||||
|
redactedMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ValidationViolation("Password was not set as it violated configured password " +
|
||||||
|
"strength policy. You may also use 'GENERATED PASSWORD' upon role " +
|
||||||
|
"creation or alteration.",
|
||||||
|
redactedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class CustomLowerCaseCharacterData implements CharacterData
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public String getErrorCode()
|
||||||
|
{
|
||||||
|
return "INSUFFICIENT_LOWERCASE";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCharacters()
|
||||||
|
{
|
||||||
|
return EnglishCharacterData.LowerCase.getCharacters() +
|
||||||
|
CyrillicCharacterData.LowerCase.getCharacters() +
|
||||||
|
CyrillicModernCharacterData.LowerCase.getCharacters() +
|
||||||
|
CzechCharacterData.LowerCase.getCharacters() +
|
||||||
|
GermanCharacterData.LowerCase.getCharacters() +
|
||||||
|
PolishCharacterData.LowerCase.getCharacters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class CustomUpperCaseCharacterData implements CharacterData
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public String getErrorCode()
|
||||||
|
{
|
||||||
|
return "INSUFFICIENT_UPPERCASE";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCharacters()
|
||||||
|
{
|
||||||
|
return EnglishCharacterData.UpperCase.getCharacters() +
|
||||||
|
CyrillicCharacterData.UpperCase.getCharacters() +
|
||||||
|
CyrillicModernCharacterData.UpperCase.getCharacters() +
|
||||||
|
CzechCharacterData.UpperCase.getCharacters() +
|
||||||
|
GermanCharacterData.UpperCase.getCharacters() +
|
||||||
|
PolishCharacterData.UpperCase.getCharacters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final CharacterData specialCharacters = new CharacterData()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public String getErrorCode()
|
||||||
|
{
|
||||||
|
return "INSUFFICIENT_SPECIAL";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCharacters()
|
||||||
|
{
|
||||||
|
return "!\"#$%&()*+,-./:;<=>?@[\\]^_`{|}~";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected List<SequenceData> getSequenceData()
|
||||||
|
{
|
||||||
|
return List.of(EnglishSequenceData.Alphabetical,
|
||||||
|
EnglishSequenceData.Numerical,
|
||||||
|
EnglishSequenceData.USQwerty,
|
||||||
|
CyrillicSequenceData.Alphabetical,
|
||||||
|
CyrillicModernSequenceData.Alphabetical,
|
||||||
|
CzechSequenceData.Alphabetical,
|
||||||
|
GermanSequenceData.Alphabetical,
|
||||||
|
PolishSequenceData.Alphabetical);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UnsupportedCharsetRule implements Rule
|
||||||
|
{
|
||||||
|
private static final char[] supportedAlphabeticChars = UnsupportedCharsetRule.supportedChars(true);
|
||||||
|
private static final char[] allSupportedCharsChars = UnsupportedCharsetRule.supportedChars(false);
|
||||||
|
|
||||||
|
public static final String ERROR_CODE = "UNSUPPORTED_CHARSET";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuleResult validate(final PasswordData passwordData)
|
||||||
|
{
|
||||||
|
final String text = passwordData.getPassword();
|
||||||
|
|
||||||
|
final RuleResult result = new RuleResult();
|
||||||
|
if (text.isEmpty())
|
||||||
|
return result;
|
||||||
|
|
||||||
|
int unsupportedChars = 0;
|
||||||
|
int supportedNonAlphabeticChars = 0;
|
||||||
|
for (char c : text.toCharArray())
|
||||||
|
{
|
||||||
|
if (Arrays.binarySearch(allSupportedCharsChars, c) < 0)
|
||||||
|
unsupportedChars++;
|
||||||
|
else if (Arrays.binarySearch(supportedAlphabeticChars, c) < 0)
|
||||||
|
supportedNonAlphabeticChars++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unsupportedChars > 0)
|
||||||
|
{
|
||||||
|
if (unsupportedChars + supportedNonAlphabeticChars == text.length())
|
||||||
|
{
|
||||||
|
result.setValid(false);
|
||||||
|
result.addError(ERROR_CODE, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
result.setMetadata(new RuleResultMetadata(RuleResultMetadata.CountCategory.Illegal, unsupportedChars));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static char[] supportedChars(boolean onlyAlphabetic)
|
||||||
|
{
|
||||||
|
char[] charArray = getChars(onlyAlphabetic);
|
||||||
|
|
||||||
|
Set<Character> charactersSet = new HashSet<>();
|
||||||
|
|
||||||
|
// filter out the duplicates
|
||||||
|
for (int i = 0; i < charArray.length; i++)
|
||||||
|
charactersSet.add(charArray[i]);
|
||||||
|
|
||||||
|
char[] result = new char[charactersSet.size()];
|
||||||
|
|
||||||
|
Iterator<Character> characterIterator = charactersSet.iterator();
|
||||||
|
int i = 0;
|
||||||
|
while (characterIterator.hasNext())
|
||||||
|
result[i++] = characterIterator.next();
|
||||||
|
|
||||||
|
Arrays.sort(result); // important for binary search
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static char[] getChars(boolean onlyAlphabetic)
|
||||||
|
{
|
||||||
|
if (onlyAlphabetic)
|
||||||
|
return (new CassandraPasswordValidator.CustomUpperCaseCharacterData().getCharacters() +
|
||||||
|
new CassandraPasswordValidator.CustomLowerCaseCharacterData().getCharacters()).toCharArray();
|
||||||
|
else
|
||||||
|
return (new CassandraPasswordValidator.CustomUpperCaseCharacterData().getCharacters() +
|
||||||
|
new CassandraPasswordValidator.CustomLowerCaseCharacterData().getCharacters() +
|
||||||
|
CassandraPasswordValidator.specialCharacters.getCharacters() +
|
||||||
|
"0123456789").toCharArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* 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.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.apache.cassandra.db.guardrails.ValueValidator.ValidationViolation;
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
import org.apache.cassandra.service.ClientState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom guardrail represents a way how to validate arbitrary values. Values are validated by an instance of
|
||||||
|
* a {@link ValueValidator}. A validator is instantiated upon node's start. If {@link Guardrails} enables it,
|
||||||
|
* it is possible to reconfigure a custom guardrail via JMX. JMX reconfiguration
|
||||||
|
* mechanism has to eventually call {@link CustomGuardrail#reconfigure(Map)} to achieve that.
|
||||||
|
* <p>
|
||||||
|
* Some custom guardrails are not meant to be reconfigurable in runtime. In that case, {@link Guardrails} should not
|
||||||
|
* provide any way to do so.
|
||||||
|
*
|
||||||
|
* @param <VALUE> type of the value a validator for this guardrail validates.
|
||||||
|
*/
|
||||||
|
public class CustomGuardrail<VALUE> extends Guardrail
|
||||||
|
{
|
||||||
|
private volatile Holder<VALUE> holder;
|
||||||
|
protected final Supplier<CustomGuardrailConfig> configSupplier;
|
||||||
|
private final boolean guardWhileSuperuser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name name of the custom guardrail
|
||||||
|
* @param configSupplier configuration supplier of the custom guardrail
|
||||||
|
* @param guardWhileSuperuser when true, the guardrail will be executed even the caller is a superuser. If
|
||||||
|
* false, this guardrail will be called only in case a caller is not a superuser.
|
||||||
|
*/
|
||||||
|
public CustomGuardrail(String name,
|
||||||
|
String reason,
|
||||||
|
Supplier<CustomGuardrailConfig> configSupplier,
|
||||||
|
boolean guardWhileSuperuser)
|
||||||
|
{
|
||||||
|
super(name, reason);
|
||||||
|
|
||||||
|
this.configSupplier = configSupplier;
|
||||||
|
this.guardWhileSuperuser = guardWhileSuperuser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueValidator<VALUE> getValidator()
|
||||||
|
{
|
||||||
|
maybeInitialize();
|
||||||
|
return holder.validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueGenerator<VALUE> getGenerator()
|
||||||
|
{
|
||||||
|
maybeInitialize();
|
||||||
|
return holder.generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean enabled(@Nullable ClientState state)
|
||||||
|
{
|
||||||
|
return guardWhileSuperuser ? super.enabled(null) : super.enabled(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value value to validate by the validator of this guardrail
|
||||||
|
* @param state client's state
|
||||||
|
*/
|
||||||
|
public void guard(VALUE value, ClientState state)
|
||||||
|
{
|
||||||
|
if (!enabled(state))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ValueValidator<VALUE> currentValidator = getValidator();
|
||||||
|
boolean calledBySuperuser = isCalledBySuperuser(state);
|
||||||
|
Optional<ValidationViolation> maybeViolation = currentValidator.shouldFail(value, calledBySuperuser);
|
||||||
|
|
||||||
|
if (maybeViolation.isPresent())
|
||||||
|
fail(maybeViolation.get().message,
|
||||||
|
maybeViolation.get().redactedMessage,
|
||||||
|
state);
|
||||||
|
else
|
||||||
|
currentValidator.shouldWarn(value, calledBySuperuser).ifPresent(result -> warn(result.message, result.redactedMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCalledBySuperuser(ClientState clientState)
|
||||||
|
{
|
||||||
|
return clientState != null && clientState.getUser() != null && clientState.getUser().isSuper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return unmodifiable view of the configuration parameters of underlying value validator.
|
||||||
|
*/
|
||||||
|
public CustomGuardrailConfig getConfig()
|
||||||
|
{
|
||||||
|
return getValidator().getParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a value of given size.
|
||||||
|
*
|
||||||
|
* @param size size of value to be generated
|
||||||
|
* @return generated value of given size
|
||||||
|
*/
|
||||||
|
public VALUE generate(int size)
|
||||||
|
{
|
||||||
|
return getGenerator().generate(size, getValidator());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a valid value.
|
||||||
|
*
|
||||||
|
* @return generated and valid value
|
||||||
|
*/
|
||||||
|
public VALUE generate()
|
||||||
|
{
|
||||||
|
return getGenerator().generate(getValidator());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconfigures this custom guardrail. After the successful finish of this method, every
|
||||||
|
* new call to this guardrail will use new configuration for its validator.
|
||||||
|
* <p>
|
||||||
|
* New configuration is merged into the old one. Values for the keys in the old configuration
|
||||||
|
* are replaced by the values of the same key in the new configuration.
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* @param newConfig if null or the configuration is an empty map, no reconfiguration happens.
|
||||||
|
* @throws ConfigurationException when new validator can not replace the old one or when it is not possible
|
||||||
|
* to instantiate new validator or generator.
|
||||||
|
*/
|
||||||
|
void reconfigure(@Nullable Map<String, Object> newConfig)
|
||||||
|
{
|
||||||
|
Map<String, Object> mergedMap = new HashMap<>(getValidator().getParameters());
|
||||||
|
|
||||||
|
if (newConfig != null)
|
||||||
|
mergedMap.putAll(newConfig);
|
||||||
|
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig(mergedMap);
|
||||||
|
|
||||||
|
ValueValidator<VALUE> newValidator = ValueValidator.getValidator(name, config);
|
||||||
|
ValueGenerator<VALUE> newGenerator = ValueGenerator.getGenerator(name, config);
|
||||||
|
|
||||||
|
holder = new Holder<>(newValidator, newGenerator);
|
||||||
|
|
||||||
|
logger.info("Reconfigured validator {} for guardrail '{}' with parameters {}", holder.validator.getClass(), name, holder.validator.getParameters());
|
||||||
|
logger.info("Reconfigured generator {} for guardrail '{}' with parameters {}", holder.generator.getClass(), name, holder.generator.getParameters());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeInitialize()
|
||||||
|
{
|
||||||
|
if (holder == null)
|
||||||
|
holder = new Holder<>(ValueValidator.getValidator(name, configSupplier.get()),
|
||||||
|
ValueGenerator.getGenerator(name, configSupplier.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Holder<VALUE>
|
||||||
|
{
|
||||||
|
final ValueValidator<VALUE> validator;
|
||||||
|
final ValueGenerator<VALUE> generator;
|
||||||
|
|
||||||
|
public Holder(ValueValidator<VALUE> validator, ValueGenerator<VALUE> generator)
|
||||||
|
{
|
||||||
|
this.validator = validator;
|
||||||
|
this.generator = generator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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 javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
public class CustomGuardrailConfig extends HashMap<String, Object>
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CustomGuardrailConfig.class);
|
||||||
|
|
||||||
|
public CustomGuardrailConfig()
|
||||||
|
{
|
||||||
|
// for snakeyaml
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public CustomGuardrailConfig(Map<String, ?> p)
|
||||||
|
{
|
||||||
|
super(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveString(@Nullable String key)
|
||||||
|
{
|
||||||
|
return resolveString(key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveString(@Nullable String key, String defaultValue)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
return defaultValue;
|
||||||
|
|
||||||
|
Object resolvedString = getOrDefault(key, defaultValue);
|
||||||
|
|
||||||
|
if (resolvedString == null)
|
||||||
|
return null;
|
||||||
|
if (resolvedString instanceof String)
|
||||||
|
return (String) resolvedString;
|
||||||
|
|
||||||
|
return resolvedString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int resolveInteger(@Nullable String key, Integer defaultValue)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
return defaultValue;
|
||||||
|
|
||||||
|
Object resolvedValue = getOrDefault(key, defaultValue.toString());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (resolvedValue instanceof Integer)
|
||||||
|
return (Integer) resolvedValue;
|
||||||
|
if (resolvedValue instanceof String)
|
||||||
|
return Integer.parseInt((String) resolvedValue);
|
||||||
|
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
catch (IllegalStateException | NumberFormatException ex)
|
||||||
|
{
|
||||||
|
logger.warn(format("Unable to parse value %s of key %s. Value has to be integer. " +
|
||||||
|
"The default of value %s will be used.",
|
||||||
|
resolvedValue, key, defaultValue));
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean resolveBoolean(@Nullable String key, boolean defaultValue)
|
||||||
|
{
|
||||||
|
Object value = get(key);
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
return defaultValue;
|
||||||
|
if (value instanceof Boolean)
|
||||||
|
return (boolean) value;
|
||||||
|
if (value instanceof String)
|
||||||
|
return Boolean.parseBoolean((String) value);
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ public abstract class Guardrail
|
||||||
private volatile long lastFailInMs = 0;
|
private volatile long lastFailInMs = 0;
|
||||||
|
|
||||||
/** Should throw exception if null client state is provided. */
|
/** Should throw exception if null client state is provided. */
|
||||||
private volatile boolean throwOnNullClientState = false;
|
protected volatile boolean throwOnNullClientState = false;
|
||||||
|
|
||||||
Guardrail(String name, @Nullable String reason)
|
Guardrail(String name, @Nullable String reason)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
package org.apache.cassandra.db.guardrails;
|
package org.apache.cassandra.db.guardrails;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
@ -450,6 +451,11 @@ public final class Guardrails implements GuardrailsMBean
|
||||||
(isWarning, value) ->
|
(isWarning, value) ->
|
||||||
isWarning ? "Replica disk usage exceeds warning threshold"
|
isWarning ? "Replica disk usage exceeds warning threshold"
|
||||||
: "Write request failed because disk usage exceeds failure threshold");
|
: "Write request failed because disk usage exceeds failure threshold");
|
||||||
|
/**
|
||||||
|
* Guardrail on passwords for CREATE / ALTER ROLE statements.
|
||||||
|
*/
|
||||||
|
public static final PasswordGuardrail password =
|
||||||
|
new PasswordGuardrail(() -> CONFIG_PROVIDER.getOrCreate(null).getPasswordValidatorConfig());
|
||||||
|
|
||||||
static
|
static
|
||||||
{
|
{
|
||||||
|
|
@ -489,8 +495,8 @@ public final class Guardrails implements GuardrailsMBean
|
||||||
state -> maximumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMaximumTimestampWarnThreshold()),
|
state -> maximumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMaximumTimestampWarnThreshold()),
|
||||||
state -> maximumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMaximumTimestampFailThreshold()),
|
state -> maximumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMaximumTimestampFailThreshold()),
|
||||||
(isWarning, what, value, threshold) ->
|
(isWarning, what, value, threshold) ->
|
||||||
format("The modification to table %s has a timestamp %s after the maximum allowable %s threshold %s",
|
format("The modification to table %s has a timestamp %s after the maximum allowable %s threshold %s",
|
||||||
what, value, isWarning ? "warning" : "failure", threshold));
|
what, value, isWarning ? "warning" : "failure", threshold));
|
||||||
|
|
||||||
public static final MinThreshold minimumAllowableTimestamp =
|
public static final MinThreshold minimumAllowableTimestamp =
|
||||||
new MinThreshold("minimum_timestamp",
|
new MinThreshold("minimum_timestamp",
|
||||||
|
|
@ -1178,6 +1184,18 @@ public final class Guardrails implements GuardrailsMBean
|
||||||
DEFAULT_CONFIG.setMaximumReplicationFactorThreshold(warn, fail);
|
DEFAULT_CONFIG.setMaximumReplicationFactorThreshold(warn, fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getPasswordValidatorConfig()
|
||||||
|
{
|
||||||
|
return password.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reconfigurePasswordValidator(Map<String, Object> config)
|
||||||
|
{
|
||||||
|
password.reconfigure(config);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDataDiskUsagePercentageWarnThreshold()
|
public int getDataDiskUsagePercentageWarnThreshold()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -540,4 +540,9 @@ public interface GuardrailsConfig
|
||||||
* @param enabled {@code true} if a query without partition key is enabled or not
|
* @param enabled {@code true} if a query without partition key is enabled or not
|
||||||
*/
|
*/
|
||||||
void setNonPartitionRestrictedQueryEnabled(boolean enabled);
|
void setNonPartitionRestrictedQueryEnabled(boolean enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return configuration for password validation guardrail.
|
||||||
|
*/
|
||||||
|
CustomGuardrailConfig getPasswordValidatorConfig();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
package org.apache.cassandra.db.guardrails;
|
package org.apache.cassandra.db.guardrails;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
@ -904,4 +905,16 @@ public interface GuardrailsMBean
|
||||||
boolean getIntersectFilteringQueryEnabled();
|
boolean getIntersectFilteringQueryEnabled();
|
||||||
|
|
||||||
void setIntersectFilteringQueryEnabled(boolean value);
|
void setIntersectFilteringQueryEnabled(boolean value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the configuration of password validator.
|
||||||
|
*/
|
||||||
|
Map<String, Object> getPasswordValidatorConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconfigures password validator.
|
||||||
|
*
|
||||||
|
* @param config configuration of new password validator
|
||||||
|
*/
|
||||||
|
void reconfigurePasswordValidator(Map<String, Object> config);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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 javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator which does not generate any value, it has the empty configuration which is valid.
|
||||||
|
* Generators are meant to generate such values which would pass when tested against respective validators.
|
||||||
|
*/
|
||||||
|
public class NoOpGenerator<VALUE> extends ValueGenerator<VALUE>
|
||||||
|
{
|
||||||
|
private static final CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
|
||||||
|
public static NoOpGenerator INSTANCE = new NoOpGenerator<>(config);
|
||||||
|
|
||||||
|
public NoOpGenerator(CustomGuardrailConfig unused)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VALUE generate(int size, ValueValidator<VALUE> validator)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VALUE generate(ValueValidator<VALUE> validator)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CustomGuardrailConfig getParameters()
|
||||||
|
{
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateParameters() throws ConfigurationException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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.cassandra.exceptions.ConfigurationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator which does nothing when it validates a value. It never fails nor warns, it
|
||||||
|
* has the empty configuration which is valid.
|
||||||
|
*/
|
||||||
|
public class NoOpValidator<T> extends ValueValidator<T>
|
||||||
|
{
|
||||||
|
private static final CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
|
||||||
|
public NoOpValidator(CustomGuardrailConfig unused)
|
||||||
|
{
|
||||||
|
super(NoOpValidator.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ValidationViolation> shouldWarn(T value, boolean calledBySuperuser)
|
||||||
|
{
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ValidationViolation> shouldFail(T value, boolean calledBySuperUser)
|
||||||
|
{
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CustomGuardrailConfig getParameters()
|
||||||
|
{
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateParameters() throws ConfigurationException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
import org.apache.cassandra.service.ClientState;
|
||||||
|
import org.apache.cassandra.service.ClientWarn;
|
||||||
|
import org.apache.cassandra.tracing.Tracing;
|
||||||
|
|
||||||
|
public class PasswordGuardrail extends CustomGuardrail<String>
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PasswordGuardrail.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param configSupplier configuration supplier of the custom guardrail
|
||||||
|
*/
|
||||||
|
public PasswordGuardrail(Supplier<CustomGuardrailConfig> configSupplier)
|
||||||
|
{
|
||||||
|
super("password", null, configSupplier, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void warn(String message, String redactedMessage)
|
||||||
|
{
|
||||||
|
String msg = decorateMessage(message);
|
||||||
|
String redactedMsg = decorateMessage(redactedMessage);
|
||||||
|
|
||||||
|
ClientWarn.instance.warn(msg);
|
||||||
|
Tracing.trace(redactedMsg);
|
||||||
|
GuardrailsDiagnostics.warned(name, redactedMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void fail(String message, String redactedMessage, @Nullable ClientState state)
|
||||||
|
{
|
||||||
|
String msg = decorateMessage(message);
|
||||||
|
String redactedMsg = decorateMessage(redactedMessage);
|
||||||
|
|
||||||
|
ClientWarn.instance.warn(msg);
|
||||||
|
Tracing.trace(redactedMsg);
|
||||||
|
GuardrailsDiagnostics.failed(name, redactedMsg);
|
||||||
|
|
||||||
|
if (state != null || throwOnNullClientState)
|
||||||
|
throw new PasswordGuardrailException(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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 javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public ValueGenerator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a value of given size.
|
||||||
|
*
|
||||||
|
* @param size size of value to be generated
|
||||||
|
* @param validator validator to validate generated value with
|
||||||
|
* @return generated value of given size
|
||||||
|
*/
|
||||||
|
public abstract VALUE generate(int size, ValueValidator<VALUE> validator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a valid value.
|
||||||
|
*
|
||||||
|
* @param validator validator to validate generated value with
|
||||||
|
* @return generated and valid value
|
||||||
|
*/
|
||||||
|
public abstract VALUE generate(ValueValidator<VALUE> validator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return parameters for this generator
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public abstract CustomGuardrailConfig getParameters();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates parameters for this generator.
|
||||||
|
*
|
||||||
|
* @throws ConfigurationException in case configuration for this generator is invalid
|
||||||
|
*/
|
||||||
|
public abstract void validateParameters() throws ConfigurationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of a validator according to the parameters in {@code config}.
|
||||||
|
*
|
||||||
|
* @param name name of a guardrail a generator is created for
|
||||||
|
* @param config configuration to instantiate a generator with. After a generator is instantiated, it will
|
||||||
|
* validate this configuration internally and throw an exception if such configuration is invalid.
|
||||||
|
* @return instance of a generator of class as specified under key {@code class_name} in {@code config}. If not set,
|
||||||
|
* {@link NoOpGenerator} will be used.
|
||||||
|
* @throws ConfigurationException thrown in case {@code config} for the constructed generator is invalid, or it was
|
||||||
|
* not possible to construct a generator.
|
||||||
|
*/
|
||||||
|
public static <VALUE> ValueGenerator<VALUE> getGenerator(String name, @Nonnull CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
String className = config.resolveString(GENERATOR_CLASS_NAME_KEY);
|
||||||
|
|
||||||
|
if (className == null || className.isEmpty())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!className.contains("."))
|
||||||
|
className = DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE + '.' + className;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Class<? extends ValueGenerator<VALUE>> generatorClass =
|
||||||
|
FBUtilities.classForName(className, "generator");
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ValueGenerator<VALUE> generator = generatorClass.getConstructor(CustomGuardrailConfig.class)
|
||||||
|
.newInstance(config);
|
||||||
|
logger.debug("Using {} generator for guardrail '{}' with parameters {}",
|
||||||
|
generator.getClass(), name, generator.getParameters());
|
||||||
|
return generator;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
String message;
|
||||||
|
if (ex.getCause() instanceof ConfigurationException)
|
||||||
|
message = ex.getCause().getMessage();
|
||||||
|
else
|
||||||
|
message = ex.getMessage();
|
||||||
|
|
||||||
|
throw new ConfigurationException(format("Unable to create instance of generator of class %s: %s",
|
||||||
|
className, message), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* 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.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
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.
|
||||||
|
* These methods are called from {@link CustomGuardrail} and CQL request either emits a failure when a value is invalid
|
||||||
|
* or a warning when it is still not valid, but it validates against the least strict validation.
|
||||||
|
*
|
||||||
|
* @param <VALUE> type parameter of a value this validator validates.
|
||||||
|
*/
|
||||||
|
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())));
|
||||||
|
|
||||||
|
private static final String DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE = ValueValidator.class.getPackage().getName();
|
||||||
|
|
||||||
|
protected final CustomGuardrailConfig config;
|
||||||
|
|
||||||
|
public ValueValidator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ValidationViolation
|
||||||
|
{
|
||||||
|
public final String message;
|
||||||
|
public final String redactedMessage;
|
||||||
|
|
||||||
|
public ValidationViolation(String message)
|
||||||
|
{
|
||||||
|
this(message, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationViolation(String message, String redactedMessage)
|
||||||
|
{
|
||||||
|
this.message = message;
|
||||||
|
this.redactedMessage = redactedMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a value to see if it emits warnings.
|
||||||
|
*
|
||||||
|
* @param value value to validate
|
||||||
|
* @param calledBySuperuser client state
|
||||||
|
* @return if optional is empty, value is valid, otherwise it returns warning violation message
|
||||||
|
*/
|
||||||
|
public abstract Optional<ValidationViolation> shouldWarn(VALUE value, boolean calledBySuperuser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a value to see if it emits failures.
|
||||||
|
*
|
||||||
|
* @param value value to validate
|
||||||
|
* @param calledBySuperUser whether this is called by a super-user or not
|
||||||
|
* @return if optional is empty, value is valid, otherwise it returns failure violation message
|
||||||
|
*/
|
||||||
|
public abstract Optional<ValidationViolation> shouldFail(VALUE value, boolean calledBySuperUser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates parameters for this validator.
|
||||||
|
*
|
||||||
|
* @throws ConfigurationException in case configuration for this validator is invalid
|
||||||
|
*/
|
||||||
|
public abstract void validateParameters() throws ConfigurationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return parameters for this validator
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public abstract CustomGuardrailConfig getParameters();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of a validator according to the parameters in {@code config}.
|
||||||
|
*
|
||||||
|
* @param name name of a guardrail a validator is created for
|
||||||
|
* @param config configuration to instantiate a validator with. After a validator is instantiated, it will
|
||||||
|
* validate this configuration internally and throw an exception if such configuration is invalid.
|
||||||
|
* @return instance of a validator of class as specified under key {@code class_name} in {@code config}. If not set,
|
||||||
|
* {@link NoOpValidator} will be used.
|
||||||
|
* @throws ConfigurationException thrown in case {@code config} for the constructed validator is invalid, or it was
|
||||||
|
* not possible to construct a validator.
|
||||||
|
*/
|
||||||
|
public static <VALUE> ValueValidator<VALUE> getValidator(String name, @Nonnull CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
String className = config.resolveString(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!className.contains("."))
|
||||||
|
className = DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE + '.' + className;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Class<? extends ValueValidator<VALUE>> validatorClass =
|
||||||
|
FBUtilities.classForName(className, "validator");
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ValueValidator<VALUE> validator = validatorClass.getConstructor(CustomGuardrailConfig.class)
|
||||||
|
.newInstance(config);
|
||||||
|
logger.debug("Using {} validator for guardrail '{}' with parameters {}",
|
||||||
|
validator.getClass(), name, validator.getParameters());
|
||||||
|
return validator;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
String message;
|
||||||
|
if (ex.getCause() instanceof ConfigurationException)
|
||||||
|
message = ex.getCause().getMessage();
|
||||||
|
else
|
||||||
|
message = ex.getMessage();
|
||||||
|
|
||||||
|
throw new ConfigurationException(format("Unable to create instance of validator of class %s: %s",
|
||||||
|
className, message), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* 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.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.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.distributed.Cluster.build;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
public class GuardrailPasswordTest 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);
|
||||||
|
|
||||||
|
try (Cluster ignored = build(1)
|
||||||
|
.withConfig(c -> c.with(Feature.NETWORK, Feature.NATIVE_PROTOCOL, Feature.GOSSIP)
|
||||||
|
.set("password_validator", 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("class_name", CassandraPasswordValidator.class.getName());
|
||||||
|
config.put("generator_class_name", CassandraPasswordGenerator.class.getName());
|
||||||
|
|
||||||
|
try (Cluster cluster = build(1)
|
||||||
|
.withConfig(c -> c.with(Feature.JMX)
|
||||||
|
.set("password_validator_reconfiguration_enabled", false)
|
||||||
|
.set("password_validator", 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() });
|
||||||
|
|
||||||
|
assertFalse(cluster.get(1)
|
||||||
|
.logs()
|
||||||
|
.watchFor("It is not possible to reconfigure password guardrail because " +
|
||||||
|
"property 'password_validator_reconfiguration_enabled' is set to false.")
|
||||||
|
.getResult()
|
||||||
|
.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,6 +68,16 @@ public class RoleOptionsTest
|
||||||
opts.setOption(IRoleManager.Option.HASHED_PASSWORD, "$2a$10$JSJEMFm6GeaW9XxT5JIheuEtPvat6i7uKbnTcxX3c1wshIIsGyUtG");
|
opts.setOption(IRoleManager.Option.HASHED_PASSWORD, "$2a$10$JSJEMFm6GeaW9XxT5JIheuEtPvat6i7uKbnTcxX3c1wshIIsGyUtG");
|
||||||
assertInvalidOptions(opts, "Properties 'PASSWORD' and 'HASHED_PASSWORD' are mutually exclusive");
|
assertInvalidOptions(opts, "Properties 'PASSWORD' and 'HASHED_PASSWORD' are mutually exclusive");
|
||||||
|
|
||||||
|
opts = new RoleOptions();
|
||||||
|
opts.setOption(IRoleManager.Option.GENERATED_PASSWORD, true);
|
||||||
|
opts.setOption(IRoleManager.Option.HASHED_PASSWORD, "$2a$10$JSJEMFm6GeaW9XxT5JIheuEtPvat6i7uKbnTcxX3c1wshIIsGyUtG");
|
||||||
|
assertInvalidOptions(opts, "Properties 'HASHED_PASSWORD' and 'GENERATED_PASSWORD' are mutually exclusive");
|
||||||
|
|
||||||
|
opts = new RoleOptions();
|
||||||
|
opts.setOption(IRoleManager.Option.PASSWORD, "abc");
|
||||||
|
opts.setOption(IRoleManager.Option.GENERATED_PASSWORD, true);
|
||||||
|
assertInvalidOptions(opts, "Properties 'PASSWORD' and 'GENERATED_PASSWORD' are mutually exclusive");
|
||||||
|
|
||||||
opts = new RoleOptions();
|
opts = new RoleOptions();
|
||||||
opts.setOption(IRoleManager.Option.LOGIN, true);
|
opts.setOption(IRoleManager.Option.LOGIN, true);
|
||||||
opts.setOption(IRoleManager.Option.SUPERUSER, false);
|
opts.setOption(IRoleManager.Option.SUPERUSER, false);
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ public class DatabaseDescriptorRefTest
|
||||||
"org.apache.cassandra.db.commitlog.CommitLogSegmentManagerFactory",
|
"org.apache.cassandra.db.commitlog.CommitLogSegmentManagerFactory",
|
||||||
"org.apache.cassandra.db.commitlog.CommitLogSegmentManagerStandard",
|
"org.apache.cassandra.db.commitlog.CommitLogSegmentManagerStandard",
|
||||||
"org.apache.cassandra.db.commitlog.DefaultCommitLogSegmentMgrFactory",
|
"org.apache.cassandra.db.commitlog.DefaultCommitLogSegmentMgrFactory",
|
||||||
|
"org.apache.cassandra.db.guardrails.CustomGuardrailConfig",
|
||||||
"org.apache.cassandra.db.guardrails.GuardrailsConfig",
|
"org.apache.cassandra.db.guardrails.GuardrailsConfig",
|
||||||
"org.apache.cassandra.db.guardrails.GuardrailsConfig$ConsistencyLevels",
|
"org.apache.cassandra.db.guardrails.GuardrailsConfig$ConsistencyLevels",
|
||||||
"org.apache.cassandra.db.guardrails.GuardrailsConfig$TableProperties",
|
"org.apache.cassandra.db.guardrails.GuardrailsConfig$TableProperties",
|
||||||
|
|
|
||||||
|
|
@ -2375,6 +2375,12 @@ public abstract class CQLTester
|
||||||
void apply() throws Throwable;
|
void apply() throws Throwable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface CheckedSupplier
|
||||||
|
{
|
||||||
|
ResultMessage get() throws Throwable;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the given function before and after a flush of sstables. This is useful for checking that behavior is
|
* Runs the given function before and after a flush of sstables. This is useful for checking that behavior is
|
||||||
* the same whether data is in memtables or sstables.
|
* the same whether data is in memtables or sstables.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.passay.CharacterRule;
|
||||||
|
import org.passay.EnglishCharacterData;
|
||||||
|
import org.passay.LengthRule;
|
||||||
|
import org.passay.PasswordData;
|
||||||
|
import org.passay.Rule;
|
||||||
|
import org.passay.RuleResultDetail;
|
||||||
|
|
||||||
|
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.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class CassandraPasswordGeneratorTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void testPasswordGenerator()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||||
|
CassandraPasswordGenerator generator = new CassandraPasswordGenerator(config);
|
||||||
|
|
||||||
|
PasswordData passwordData = new PasswordData(generator.generate(validator));
|
||||||
|
for (Rule rule : validator.warnValidator.getRules())
|
||||||
|
assertTrue(rule.validate(passwordData).isValid());
|
||||||
|
|
||||||
|
for (Rule rule : validator.failValidator.getRules())
|
||||||
|
assertTrue(rule.validate(passwordData).isValid());
|
||||||
|
|
||||||
|
assertTrue(validator.shouldWarn(passwordData.getPassword(), true).isEmpty());
|
||||||
|
assertTrue(validator.shouldFail(passwordData.getPassword(), true).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordGenerationLength()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(LENGTH_WARN_KEY, 20);
|
||||||
|
config.put(LENGTH_FAIL_KEY, 15);
|
||||||
|
|
||||||
|
CassandraPasswordGenerator generator = new CassandraPasswordGenerator(config);
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||||
|
|
||||||
|
assertEquals(20, generator.generate(validator).length());
|
||||||
|
assertEquals(30, generator.generate(30, validator).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordGenerationOfLengthViolatingThreshold()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(LENGTH_WARN_KEY, 20);
|
||||||
|
config.put(LENGTH_FAIL_KEY, 15);
|
||||||
|
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||||
|
|
||||||
|
PasswordData passwordData = new PasswordData("13gE.^Tg$sSd%Tx34.w");
|
||||||
|
|
||||||
|
for (Rule rule : validator.warnValidator.getRules())
|
||||||
|
{
|
||||||
|
if (rule instanceof LengthRule)
|
||||||
|
{
|
||||||
|
assertFalse(rule.validate(passwordData).isValid());
|
||||||
|
List<RuleResultDetail> details = rule.validate(passwordData).getDetails();
|
||||||
|
assertEquals(1, details.size());
|
||||||
|
RuleResultDetail ruleResultDetail = details.get(0);
|
||||||
|
assertEquals("TOO_SHORT", ruleResultDetail.getErrorCode());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assertTrue(rule.validate(passwordData).isValid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Rule rule : validator.failValidator.getRules())
|
||||||
|
assertTrue(rule.validate(passwordData).isValid());
|
||||||
|
|
||||||
|
assertFalse(validator.shouldWarn(passwordData.getPassword(), true).isEmpty());
|
||||||
|
assertTrue(validator.shouldFail(passwordData.getPassword(), true).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGeneratedPasswordWithoutSpecialCharsIsInvalidWithDefaultValidator()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||||
|
CassandraPasswordGenerator testGenerator = new TestPasswordGenerator(config);
|
||||||
|
Assertions.assertThatThrownBy(() -> testGenerator.generate(validator))
|
||||||
|
.hasMessageContaining("It was not possible to generate a valid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestPasswordGenerator extends CassandraPasswordGenerator
|
||||||
|
{
|
||||||
|
public TestPasswordGenerator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<CharacterRule> getCharacterGenerationRules(int upper, int lower, int digits, int special)
|
||||||
|
{
|
||||||
|
// omit special chars rule on purpose
|
||||||
|
return Arrays.asList(new CharacterRule(EnglishCharacterData.UpperCase, upper),
|
||||||
|
new CharacterRule(EnglishCharacterData.LowerCase, lower),
|
||||||
|
new CharacterRule(EnglishCharacterData.Digit, digits));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,369 @@
|
||||||
|
/*
|
||||||
|
* 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.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.db.guardrails.ValueValidator.ValidationViolation;
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
import org.passay.IllegalSequenceRule;
|
||||||
|
|
||||||
|
import static java.lang.Boolean.FALSE;
|
||||||
|
import static java.lang.Boolean.TRUE;
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_CHARACTERISTIC_FAIL;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_ILLEGAL_SEQUENCE_LENGTH;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_CHARACTERISTIC_WARN;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_LENGTH_FAIL;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_LENGTH_WARN;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_LOWER_CASE_FAIL;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_LOWER_CASE_WARN;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_SPECIAL_FAIL;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_SPECIAL_WARN;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_UPPER_CASE_FAIL;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DEFAULT_UPPER_CASE_WARN;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.ILLEGAL_SEQUENCE_LENGTH_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.MAX_CHARACTERISTICS;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.MAX_LENGTH_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.CHARACTERISTIC_FAIL_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.CHARACTERISTIC_WARN_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DIGIT_FAIL_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.DIGIT_WARN_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LENGTH_FAIL_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LENGTH_WARN_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LOWER_CASE_FAIL_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.LOWER_CASE_WARN_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.SPECIAL_FAIL_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.SPECIAL_WARN_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.UPPER_CASE_FAIL_KEY;
|
||||||
|
import static org.apache.cassandra.db.guardrails.CassandraPasswordConfiguration.UPPER_CASE_WARN_KEY;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class CassandraPasswordValidatorTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void testEmptyConfigIsValid()
|
||||||
|
{
|
||||||
|
new CassandraPasswordValidator(new CustomGuardrailConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultConfiguration()
|
||||||
|
{
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(new CustomGuardrailConfig());
|
||||||
|
CustomGuardrailConfig parameters = validator.getParameters();
|
||||||
|
CassandraPasswordConfiguration conf = new CassandraPasswordConfiguration(parameters);
|
||||||
|
|
||||||
|
assertEquals(DEFAULT_CHARACTERISTIC_WARN, conf.characteristicsWarn);
|
||||||
|
assertEquals(DEFAULT_CHARACTERISTIC_FAIL, conf.characteristicsFail);
|
||||||
|
|
||||||
|
assertEquals(DEFAULT_LENGTH_WARN, conf.lengthWarn);
|
||||||
|
assertEquals(DEFAULT_LENGTH_FAIL, conf.lengthFail);
|
||||||
|
|
||||||
|
assertEquals(DEFAULT_UPPER_CASE_WARN, conf.upperCaseWarn);
|
||||||
|
assertEquals(DEFAULT_UPPER_CASE_FAIL, conf.upperCaseFail);
|
||||||
|
|
||||||
|
assertEquals(DEFAULT_LOWER_CASE_WARN, conf.lowerCaseWarn);
|
||||||
|
assertEquals(DEFAULT_LOWER_CASE_FAIL, conf.lowerCaseFail);
|
||||||
|
|
||||||
|
assertEquals(DEFAULT_SPECIAL_WARN, conf.specialsWarn);
|
||||||
|
assertEquals(DEFAULT_SPECIAL_FAIL, conf.specialsFail);
|
||||||
|
|
||||||
|
assertEquals(DEFAULT_ILLEGAL_SEQUENCE_LENGTH, conf.illegalSequenceLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidParameters()
|
||||||
|
{
|
||||||
|
for (int[] warnAndFail : new int[][]{ { 10, 10 }, { 5, 10 } })
|
||||||
|
{
|
||||||
|
int warn = warnAndFail[0];
|
||||||
|
int fail = warnAndFail[1];
|
||||||
|
validateWithConfig(() -> Map.of(LENGTH_WARN_KEY, warn, LENGTH_FAIL_KEY, fail),
|
||||||
|
format("%s of value %s is less or equal to %s of value %s",
|
||||||
|
LENGTH_WARN_KEY, warn, LENGTH_FAIL_KEY, fail));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(SPECIAL_WARN_KEY, warn, SPECIAL_FAIL_KEY, fail),
|
||||||
|
format("%s of value %s is less or equal to %s of value %s",
|
||||||
|
SPECIAL_WARN_KEY, warn, SPECIAL_FAIL_KEY, fail));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(DIGIT_WARN_KEY, warn, DIGIT_FAIL_KEY, fail),
|
||||||
|
format("%s of value %s is less or equal to %s of value %s",
|
||||||
|
DIGIT_WARN_KEY, warn, DIGIT_FAIL_KEY, fail));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(LOWER_CASE_WARN_KEY, warn, LOWER_CASE_FAIL_KEY, fail),
|
||||||
|
format("%s of value %s is less or equal to %s of value %s",
|
||||||
|
LOWER_CASE_WARN_KEY, warn, LOWER_CASE_FAIL_KEY, fail));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(UPPER_CASE_WARN_KEY, warn, UPPER_CASE_FAIL_KEY, fail),
|
||||||
|
format("%s of value %s is less or equal to %s of value %s",
|
||||||
|
UPPER_CASE_WARN_KEY, warn, UPPER_CASE_FAIL_KEY, fail));
|
||||||
|
}
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(ILLEGAL_SEQUENCE_LENGTH_KEY, 2),
|
||||||
|
format("Illegal sequence length can not be lower than %s.",
|
||||||
|
IllegalSequenceRule.MINIMUM_SEQUENCE_LENGTH));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(CHARACTERISTIC_WARN_KEY, 5),
|
||||||
|
format("%s can not be bigger than %s",
|
||||||
|
CHARACTERISTIC_WARN_KEY, MAX_CHARACTERISTICS));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(CHARACTERISTIC_FAIL_KEY, 5),
|
||||||
|
format("%s can not be bigger than %s",
|
||||||
|
CHARACTERISTIC_FAIL_KEY, MAX_CHARACTERISTICS));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(CHARACTERISTIC_WARN_KEY, 3, CHARACTERISTIC_FAIL_KEY, 3),
|
||||||
|
format("%s can not be equal to %s. You set %s and %s respectively.",
|
||||||
|
CHARACTERISTIC_FAIL_KEY, CHARACTERISTIC_WARN_KEY, 3, 3));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(CHARACTERISTIC_WARN_KEY, 3, CHARACTERISTIC_FAIL_KEY, 4),
|
||||||
|
format("%s can not be bigger than %s. You have set %s and %s respectively.",
|
||||||
|
CHARACTERISTIC_FAIL_KEY, CHARACTERISTIC_WARN_KEY, 4, 3));
|
||||||
|
|
||||||
|
validateWithConfig(() -> Map.of(SPECIAL_WARN_KEY, 1,
|
||||||
|
SPECIAL_FAIL_KEY, 0,
|
||||||
|
DIGIT_WARN_KEY, 1,
|
||||||
|
DIGIT_FAIL_KEY, 0,
|
||||||
|
UPPER_CASE_WARN_KEY, 2,
|
||||||
|
LOWER_CASE_WARN_KEY, 2,
|
||||||
|
CHARACTERISTIC_WARN_KEY, 3,
|
||||||
|
CHARACTERISTIC_FAIL_KEY, 2,
|
||||||
|
LENGTH_WARN_KEY, 3,
|
||||||
|
LENGTH_FAIL_KEY, 2),
|
||||||
|
format("The shortest password to pass the warning validator for any %s characteristics " +
|
||||||
|
"out of %s is %s but you have set the %s to %s.",
|
||||||
|
3, MAX_CHARACTERISTICS, 4, LENGTH_WARN_KEY, 3));
|
||||||
|
|
||||||
|
validateWithConfig(() ->
|
||||||
|
new HashMap<>()
|
||||||
|
{{
|
||||||
|
put(SPECIAL_FAIL_KEY, 1);
|
||||||
|
put(DIGIT_WARN_KEY, 2);
|
||||||
|
put(DIGIT_FAIL_KEY, 1);
|
||||||
|
put(UPPER_CASE_WARN_KEY, 2);
|
||||||
|
put(UPPER_CASE_FAIL_KEY, 1);
|
||||||
|
put(LOWER_CASE_WARN_KEY, 2);
|
||||||
|
put(LOWER_CASE_FAIL_KEY, 1);
|
||||||
|
put(CHARACTERISTIC_WARN_KEY, 4);
|
||||||
|
put(CHARACTERISTIC_FAIL_KEY, 3);
|
||||||
|
put(LENGTH_WARN_KEY, 8);
|
||||||
|
put(LENGTH_FAIL_KEY, 2);
|
||||||
|
}},
|
||||||
|
format("The shortest password to pass the failing validator for any %s characteristics " +
|
||||||
|
"out of %s is %s but you have set the %s to %s.",
|
||||||
|
3, MAX_CHARACTERISTICS, 3, LENGTH_FAIL_KEY, 2));
|
||||||
|
|
||||||
|
for (Map.Entry<String, Integer> entry : new HashMap<String, Integer>()
|
||||||
|
{{
|
||||||
|
put(MAX_LENGTH_KEY, -1);
|
||||||
|
put(SPECIAL_FAIL_KEY, -1);
|
||||||
|
put(DIGIT_WARN_KEY, -1);
|
||||||
|
put(DIGIT_FAIL_KEY, -1);
|
||||||
|
put(UPPER_CASE_WARN_KEY, -1);
|
||||||
|
put(UPPER_CASE_FAIL_KEY, -1);
|
||||||
|
put(LOWER_CASE_WARN_KEY, -1);
|
||||||
|
put(LOWER_CASE_FAIL_KEY, -1);
|
||||||
|
put(CHARACTERISTIC_WARN_KEY, -1);
|
||||||
|
put(CHARACTERISTIC_FAIL_KEY, -1);
|
||||||
|
put(LENGTH_WARN_KEY, -1);
|
||||||
|
put(LENGTH_FAIL_KEY, -1);
|
||||||
|
}}.entrySet())
|
||||||
|
{
|
||||||
|
validateWithConfig(() -> Map.of(entry.getKey(), entry.getValue()),
|
||||||
|
format("%s must be positive.", entry.getKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIllegalSequences()
|
||||||
|
{
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(new CustomGuardrailConfig());
|
||||||
|
Optional<ValidationViolation> validationResult = validator.shouldFail("A1$abcdefgh", true);
|
||||||
|
assertTrue(validationResult.isPresent());
|
||||||
|
assertThat(validationResult.get().message,
|
||||||
|
containsString("Password contains the illegal alphabetical sequence 'abcdefgh'."));
|
||||||
|
|
||||||
|
validationResult = validator.shouldFail("A1$a123456", true);
|
||||||
|
assertTrue(validationResult.isPresent());
|
||||||
|
assertThat(validationResult.get().message,
|
||||||
|
containsString("Password contains the illegal numerical sequence '123456'."));
|
||||||
|
|
||||||
|
validationResult = validator.shouldFail("A1$asdfghjkl", true);
|
||||||
|
assertTrue(validationResult.isPresent());
|
||||||
|
assertThat(validationResult.get().message,
|
||||||
|
containsString("Password contains the illegal QWERTY sequence 'asdfghjkl'."));
|
||||||
|
|
||||||
|
// test e.g. illegal polish sequence
|
||||||
|
validationResult = validator.shouldFail("A1$ęlłmnńoóp", true);
|
||||||
|
assertTrue(validationResult.isPresent());
|
||||||
|
assertThat(validationResult.get().message,
|
||||||
|
containsString("Password contains the illegal alphabetical sequence 'lłmnńoóp'."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWhitespace()
|
||||||
|
{
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(new CustomGuardrailConfig());
|
||||||
|
Optional<ValidationViolation> validationResult = validator.shouldFail("A1$abcd efgh", true);
|
||||||
|
assertTrue(validationResult.isPresent());
|
||||||
|
assertThat(validationResult.get().message, containsString("Password contains a whitespace character."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailingValidationResult()
|
||||||
|
{
|
||||||
|
String verboseMessage = "Password was not set as it violated configured password strength policy. " +
|
||||||
|
"To fix this error, the following has to be resolved: " +
|
||||||
|
"Password must contain 1 or more uppercase characters. " +
|
||||||
|
"Password must contain 1 or more digit characters. " +
|
||||||
|
"Password must contain 1 or more special characters. " +
|
||||||
|
"Password matches 1 of 4 character rules, but 3 are required. " +
|
||||||
|
"You may also use 'GENERATED PASSWORD' upon role creation or alteration.";
|
||||||
|
|
||||||
|
String simpleMessage = "Password was not set as it violated configured password strength policy. You may also use 'GENERATED PASSWORD' upon role creation or alteration.";
|
||||||
|
|
||||||
|
for (Object[] entry : new Object[][]{
|
||||||
|
// first boolean is if detailed messages should be provided
|
||||||
|
// second boolean is if it is called by a super-user
|
||||||
|
{ simpleMessage, FALSE, FALSE },
|
||||||
|
{ verboseMessage, FALSE, TRUE },
|
||||||
|
{ verboseMessage, TRUE, FALSE },
|
||||||
|
{ verboseMessage, TRUE, TRUE } })
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(CassandraPasswordConfiguration.DETAILED_MESSAGES_KEY, entry[1]);
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||||
|
|
||||||
|
Optional<ValidationViolation> validationResult = validator.shouldFail("acefghuiiui", (boolean) entry[2]);
|
||||||
|
assertTrue(validationResult.isPresent());
|
||||||
|
assertEquals("[INSUFFICIENT_DIGIT, INSUFFICIENT_CHARACTERISTICS, INSUFFICIENT_SPECIAL, INSUFFICIENT_UPPERCASE]",
|
||||||
|
validationResult.get().redactedMessage);
|
||||||
|
assertEquals(entry[0], validationResult.get().message);
|
||||||
|
|
||||||
|
// additionally, we satisfied special chars and digit characteristics,
|
||||||
|
// by that we satisfied 3 out of 4 character rules, so we are not emitting a failure
|
||||||
|
validationResult = validator.shouldFail("a5ef1hu.iu$", (boolean) entry[2]);
|
||||||
|
assertFalse(validationResult.isPresent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWarningValidationResult()
|
||||||
|
{
|
||||||
|
String verboseMessage = "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 contain 2 or more uppercase characters. " +
|
||||||
|
"Password must contain 2 or more digit characters. " +
|
||||||
|
"Password matches 2 of 4 character rules, but 4 are required. " +
|
||||||
|
"You may also use 'GENERATED PASSWORD' upon role creation or alteration.";
|
||||||
|
|
||||||
|
String simpleMessage = "Password was set, however it might not be strong enough according to the configured password strength policy.";
|
||||||
|
|
||||||
|
for (Object[] entry : new Object[][]{
|
||||||
|
// first boolean is if detailed messages should be provided
|
||||||
|
// second boolean is if it is called by a super-user
|
||||||
|
{ simpleMessage, FALSE, FALSE },
|
||||||
|
{ verboseMessage, FALSE, TRUE },
|
||||||
|
{ verboseMessage, TRUE, FALSE },
|
||||||
|
{ verboseMessage, TRUE, TRUE } })
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(CassandraPasswordConfiguration.DETAILED_MESSAGES_KEY, entry[1]);
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||||
|
|
||||||
|
Optional<ValidationViolation> validationResult = validator.shouldWarn("t$Efg1#a..fr", (boolean) entry[2]);
|
||||||
|
assertTrue(validationResult.isPresent());
|
||||||
|
assertEquals("[INSUFFICIENT_DIGIT, INSUFFICIENT_CHARACTERISTICS, INSUFFICIENT_UPPERCASE]",
|
||||||
|
validationResult.get().redactedMessage);
|
||||||
|
assertEquals(entry[0], validationResult.get().message);
|
||||||
|
|
||||||
|
// we added one more number and one more upper-case character,
|
||||||
|
// so we started to satisfy 4 out of 4 characteristics which does not emit any warning
|
||||||
|
assertFalse(validator.shouldWarn("A$Efg1#a..6r", (boolean) entry[2]).isPresent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateWithConfig(Supplier<Map<String, Object>> configSupplier, String expectedMessage)
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig customConfig = new CustomGuardrailConfig();
|
||||||
|
customConfig.putAll(configSupplier.get());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> new CassandraPasswordConfiguration(customConfig))
|
||||||
|
.hasMessageContaining(expectedMessage)
|
||||||
|
.isInstanceOf(ConfigurationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnicodeCharacters()
|
||||||
|
{
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(new CustomGuardrailConfig());
|
||||||
|
assertFalse(validator.shouldFail("13gE.^Tg$sSd%棚山", true).isPresent());
|
||||||
|
assertFalse(validator.shouldFail("13gE.^Tg$sSd%棚", true).isPresent());
|
||||||
|
assertFalse(validator.shouldFail("13gE.^Tg$sSd%", true).isPresent());
|
||||||
|
assertFalse(validator.shouldFail("13g.^Tg$sSd", true).isPresent());
|
||||||
|
|
||||||
|
assertFalse(validator.shouldWarn("13gE.^Tg$sSd%棚山", true).isPresent());
|
||||||
|
assertFalse(validator.shouldWarn("13gE.^Tg$sSd%棚", true).isPresent());
|
||||||
|
assertFalse(validator.shouldWarn("13gE.^Tg$sSd%", true).isPresent());
|
||||||
|
|
||||||
|
assertFalse(validator.shouldWarn("ášK&Ič[ž90Ašž", true).isPresent());
|
||||||
|
assertFalse(validator.shouldFail("ášK&Ič[ž90Ašž", true).isPresent());
|
||||||
|
|
||||||
|
// unicode characters are contributing to length attribute, but
|
||||||
|
// they are not contributing to any other rules, this password is 12 chars long,
|
||||||
|
// which is required to not emit any warning,
|
||||||
|
// it needs to contain at least:
|
||||||
|
// 2 specials (contains 3)
|
||||||
|
// 2 upper chars (contains 2)
|
||||||
|
// 2 lower chars (contains 3)
|
||||||
|
// 2 digits (contains 2)
|
||||||
|
// together 10, plus 2 unicodes = 12, so it satisfies length characteristic as well
|
||||||
|
assertFalse(validator.shouldWarn("山13g.^Tg$棚Sd", true).isPresent());
|
||||||
|
|
||||||
|
// unsupported languages should be rejected
|
||||||
|
assertTrue(validator.shouldWarn("卡桑德拉-卡桑德拉山山羊棚", true).isPresent());
|
||||||
|
assertTrue(validator.shouldFail("卡桑德拉卡桑德拉山山羊棚", true).isPresent());
|
||||||
|
assertTrue(validator.shouldWarn("لرئتينمسؤونعنلتنفس", true).isPresent());
|
||||||
|
assertTrue(validator.shouldFail("لرئتينمسؤو-نعنلتنفس", true).isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMaxLength()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
CassandraPasswordValidator validator = new CassandraPasswordValidator(config);
|
||||||
|
CassandraPasswordGenerator generator = new CassandraPasswordGenerator(config);
|
||||||
|
String password = generator.generate(1000, validator);
|
||||||
|
assertEquals(1000, password.length());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> generator.generate(1001, validator))
|
||||||
|
.isInstanceOf(ConfigurationException.class)
|
||||||
|
.hasMessageContaining("Unable to generate a password of length " + 1001);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
/*
|
||||||
|
* 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 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;
|
||||||
|
|
||||||
|
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.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
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
testPasswordGuardrailInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordGuardrailForUser() throws Throwable
|
||||||
|
{
|
||||||
|
entity = "USER";
|
||||||
|
testPasswordGuardrailInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that if there is no password validator / generator set then it is not possible to generate any passwords
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDisabledGuardrailPreventPasswordGeneration() throws Throwable
|
||||||
|
{
|
||||||
|
// 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 " +
|
||||||
|
"in cassandra.yaml to be able to generate passwords.",
|
||||||
|
null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a user uses "GENERATED PASSWORD" and "HASHED PASSWORD", such combination is not possible
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testGeneratedAndHashedPasswordCannotBeUsedTogether() throws Throwable
|
||||||
|
{
|
||||||
|
// enable password guardrail
|
||||||
|
setGuardrail(getConfig(true));
|
||||||
|
|
||||||
|
String hashedPassword = BCrypt.hashpw("doesnotmatter", BCrypt.gensalt(10));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> execute(userClientState, "CREATE ROLE role10 WITH GENERATED PASSWORD AND HASHED PASSWORD = '" + hashedPassword + '\''))
|
||||||
|
.isInstanceOf(SyntaxException.class)
|
||||||
|
.hasMessage("Options 'hashed password' and 'generated password' are mutually exclusive");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> execute(userClientState, "CREATE ROLE role10 WITH GENERATED PASSWORD AND PASSWORD = 'doesnotmatter'"))
|
||||||
|
.isInstanceOf(SyntaxException.class)
|
||||||
|
.hasMessage("Options 'password' and 'generated password' are mutually exclusive");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAllSpecialCharactersArePossibleToUseInCQLQuery()
|
||||||
|
{
|
||||||
|
setGuardrail(getConfig(true));
|
||||||
|
|
||||||
|
PasswordGuardrail guardrail = new PasswordGuardrail(() -> new CustomGuardrailConfig(getConfig(true)));
|
||||||
|
String userGeneratedPassword = guardrail.generate(20);
|
||||||
|
String allSpecialCharacters = CassandraPasswordValidator.specialCharacters.getCharacters();
|
||||||
|
String passwordWithAllSpecialChars = userGeneratedPassword + allSpecialCharacters;
|
||||||
|
|
||||||
|
String queryToExecute = "CREATE ROLE role123 WITH PASSWORD = '" + passwordWithAllSpecialChars + '\'';
|
||||||
|
execute(userClientState, queryToExecute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getEntityName(String name)
|
||||||
|
{
|
||||||
|
return (name + entity).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
boolean expectThrow,
|
||||||
|
String expectedFailureMessage,
|
||||||
|
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;
|
||||||
|
if (assertFails)
|
||||||
|
{
|
||||||
|
return assertFails(() -> execute(userClientState, queryToExecute),
|
||||||
|
expectThrow,
|
||||||
|
expectedFailureMessage == null ? List.of() : singletonList(expectedFailureMessage),
|
||||||
|
expectedRedactedMessage == null ? List.of() : singletonList(expectedRedactedMessage));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Optional.ofNullable(assertWarnsWithResult(() -> execute(userClientState, queryToExecute),
|
||||||
|
expectedFailureMessage == null ? List.of() : singletonList(expectedFailureMessage),
|
||||||
|
expectedRedactedMessage == null ? List.of() : singletonList(expectedRedactedMessage)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ValueGenerator.GENERATOR_CLASS_NAME_KEY, CassandraPasswordGenerator.class.getName());
|
||||||
|
|
||||||
|
config.put(LENGTH_FAIL_KEY, 15);
|
||||||
|
config.put(LENGTH_WARN_KEY, 20);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new CustomGuardrailConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
|
@ -287,6 +288,32 @@ public abstract class GuardrailTester extends CQLTester
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected ResultMessage assertWarnsWithResult(CheckedSupplier supplier, String message, String redactedMessage) throws Throwable
|
||||||
|
{
|
||||||
|
return assertWarnsWithResult(supplier, Collections.singletonList(message), Collections.singletonList(redactedMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ResultMessage assertWarnsWithResult(CheckedSupplier supplier, List<String> messages, List<String> redactedMessages) throws Throwable
|
||||||
|
{
|
||||||
|
// We use client warnings to check we properly warn as this is the most convenient. Technically,
|
||||||
|
// this doesn't validate we also log the warning, but that's probably fine ...
|
||||||
|
ClientWarn.instance.captureWarnings();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ResultMessage message = supplier.get();
|
||||||
|
assertWarnings(messages);
|
||||||
|
listener.assertWarned(redactedMessages);
|
||||||
|
listener.assertNotFailed();
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ClientWarn.instance.resetWarnings();
|
||||||
|
listener.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void assertFails(String query, String message) throws Throwable
|
protected void assertFails(String query, String message) throws Throwable
|
||||||
{
|
{
|
||||||
assertFails(query, message, message);
|
assertFails(query, message, message);
|
||||||
|
|
@ -341,15 +368,35 @@ public abstract class GuardrailTester extends CQLTester
|
||||||
* Unlike {@link CQLTester#assertInvalidThrowMessage}, the chain of methods ending here in {@link GuardrailTester}
|
* Unlike {@link CQLTester#assertInvalidThrowMessage}, the chain of methods ending here in {@link GuardrailTester}
|
||||||
* respect the input ClientState so guardrails permissions will be correctly checked.
|
* respect the input ClientState so guardrails permissions will be correctly checked.
|
||||||
*/
|
*/
|
||||||
protected void assertFails(CheckedFunction function, boolean thrown, List<String> messages, List<String> redactedMessages) throws Throwable
|
protected Optional<ResultMessage> assertFails(CheckedFunction function, boolean thrown, List<String> messages, List<String> redactedMessages) throws Throwable
|
||||||
|
{
|
||||||
|
return assertFailsInternal(function, thrown, messages, redactedMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Optional<ResultMessage> assertFails(CheckedSupplier supplier, boolean thrown, List<String> messages, List<String> redactedMessages) throws Throwable
|
||||||
|
{
|
||||||
|
return assertFailsInternal(supplier, thrown, messages, redactedMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ResultMessage> assertFailsInternal(Object functionOrSupplier, boolean thrown, List<String> messages, List<String> redactedMessages) throws Throwable
|
||||||
{
|
{
|
||||||
ClientWarn.instance.captureWarnings();
|
ClientWarn.instance.captureWarnings();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
function.apply();
|
ResultMessage resultMessage = null;
|
||||||
|
if (functionOrSupplier instanceof CheckedFunction)
|
||||||
|
{
|
||||||
|
((CheckedFunction) functionOrSupplier).apply();
|
||||||
|
}
|
||||||
|
else if (functionOrSupplier instanceof CheckedSupplier)
|
||||||
|
{
|
||||||
|
resultMessage = ((CheckedSupplier) functionOrSupplier).get();
|
||||||
|
}
|
||||||
|
|
||||||
if (thrown)
|
if (thrown)
|
||||||
fail("Expected to fail, but it did not");
|
fail("Expected to fail, but it did not");
|
||||||
|
|
||||||
|
return Optional.ofNullable(resultMessage);
|
||||||
}
|
}
|
||||||
catch (InvalidRequestException e) // TODO: this used to catch GuardrailViolatedException, but now we throw InvalidRequestException for all rejections in Schema#submit
|
catch (InvalidRequestException e) // TODO: this used to catch GuardrailViolatedException, but now we throw InvalidRequestException for all rejections in Schema#submit
|
||||||
{
|
{
|
||||||
|
|
@ -376,18 +423,23 @@ public abstract class GuardrailTester extends CQLTester
|
||||||
assertTrue(format("Full error message '%s' does not contain expected message '%s'", e.getMessage(), failMessage),
|
assertTrue(format("Full error message '%s' does not contain expected message '%s'", e.getMessage(), failMessage),
|
||||||
e.getMessage().contains(failMessage));
|
e.getMessage().contains(failMessage));
|
||||||
|
|
||||||
assertWarnings(messages);
|
if (e instanceof GuardrailViolatedException)
|
||||||
if (messages.size() > 1)
|
{
|
||||||
listener.assertWarned(redactedMessages.subList(0, messages.size() - 1));
|
assertWarnings(messages);
|
||||||
else
|
if (messages.size() > 1)
|
||||||
listener.assertNotWarned();
|
listener.assertWarned(redactedMessages.subList(0, messages.size() - 1));
|
||||||
listener.assertFailed(redactedMessages.get(messages.size() - 1));
|
else
|
||||||
|
listener.assertNotWarned();
|
||||||
|
listener.assertFailed(redactedMessages.get(messages.size() - 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ClientWarn.instance.resetWarnings();
|
ClientWarn.instance.resetWarnings();
|
||||||
listener.clear();
|
listener.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void assertFails(String query, String... messages) throws Throwable
|
protected void assertFails(String query, String... messages) throws Throwable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* 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.Arrays;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
public class ValueGeneratorTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void testValueGenerator()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(GENERATOR_CLASS_NAME_KEY, BooleanGenerator.class.getName());
|
||||||
|
config.put("expecting_true", true);
|
||||||
|
config.put("default_size", 10);
|
||||||
|
ValueGenerator<Boolean[]> booleanGenerator = ValueGenerator.getGenerator("boolean generator", config);
|
||||||
|
ValueValidator<Boolean[]> booleanValidator = ValueValidator.getValidator("boolean validator", config);
|
||||||
|
|
||||||
|
Boolean[] defaultBooleans = booleanGenerator.generate(booleanValidator);
|
||||||
|
assertNotNull(defaultBooleans);
|
||||||
|
assertEquals(10, defaultBooleans.length);
|
||||||
|
|
||||||
|
Boolean[] twentyBooleans = booleanGenerator.generate(20, booleanValidator);
|
||||||
|
assertEquals(20, twentyBooleans.length);
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
assertEquals(true, twentyBooleans[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidatorCreationWithInvalidConfiguration()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(GENERATOR_CLASS_NAME_KEY, BooleanGenerator.class.getName());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> ValueGenerator.getGenerator("boolean generator", config))
|
||||||
|
.isInstanceOf(ConfigurationException.class)
|
||||||
|
.message().isEqualTo(format("Unable to create instance of generator of class %s: does not contain property 'expecting_true'", BooleanGenerator.class.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BooleanGenerator extends ValueGenerator<Boolean[]>
|
||||||
|
{
|
||||||
|
private final boolean expectingTrue;
|
||||||
|
private final int defaultSize;
|
||||||
|
|
||||||
|
public BooleanGenerator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
validateParameters();
|
||||||
|
expectingTrue = (Boolean) config.get("expecting_true");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Boolean[] booleans = new Boolean[defaultSize];
|
||||||
|
Arrays.fill(booleans, expectingTrue);
|
||||||
|
return booleans;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CustomGuardrailConfig getParameters()
|
||||||
|
{
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateParameters() throws ConfigurationException
|
||||||
|
{
|
||||||
|
if (!config.containsKey("expecting_true"))
|
||||||
|
throw new ConfigurationException("does not contain property 'expecting_true'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.util.Optional;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.db.guardrails.ValueValidator.ValidationViolation;
|
||||||
|
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.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ValueValidatorTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void testValidator()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put("expecting_true", FALSE);
|
||||||
|
|
||||||
|
ValueValidator<Boolean> booleanValidator = new BooleanValidator(config);
|
||||||
|
|
||||||
|
Optional<ValidationViolation> violationResult = booleanValidator.shouldWarn(true, true);
|
||||||
|
assertTrue(violationResult.isPresent());
|
||||||
|
assertEquals("Expecting true is false", violationResult.get().message);
|
||||||
|
|
||||||
|
violationResult = booleanValidator.shouldWarn(false, true);
|
||||||
|
assertTrue(violationResult.isEmpty());
|
||||||
|
|
||||||
|
violationResult = booleanValidator.shouldWarn(true, true);
|
||||||
|
assertTrue(violationResult.isPresent());
|
||||||
|
assertEquals("Expecting true is false", violationResult.get().message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidatorCreation()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(CLASS_NAME_KEY, BooleanValidator.class.getName());
|
||||||
|
config.put("expecting_true", FALSE);
|
||||||
|
|
||||||
|
ValueValidator<Boolean> booleanValidator = ValueValidator.getValidator("boolean validator", config);
|
||||||
|
assertNotNull(booleanValidator);
|
||||||
|
|
||||||
|
Optional<ValidationViolation> violationResult = booleanValidator.shouldWarn(true, true);
|
||||||
|
assertTrue(violationResult.isPresent());
|
||||||
|
assertEquals("Expecting true is false", violationResult.get().message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidatorCreationWithInvalidConfiguration()
|
||||||
|
{
|
||||||
|
CustomGuardrailConfig config = new CustomGuardrailConfig();
|
||||||
|
config.put(CLASS_NAME_KEY, BooleanValidator.class.getName());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> ValueValidator.getValidator("boolean validator", config))
|
||||||
|
.isInstanceOf(ConfigurationException.class)
|
||||||
|
.message().isEqualTo(format("Unable to create instance of validator of class %s: does not contain property 'expecting_true'", BooleanValidator.class.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BooleanValidator extends ValueValidator<Boolean>
|
||||||
|
{
|
||||||
|
private final boolean expectingTrue;
|
||||||
|
|
||||||
|
public BooleanValidator(CustomGuardrailConfig config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
validateParameters();
|
||||||
|
expectingTrue = (Boolean) config.get("expecting_true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ValidationViolation> shouldWarn(Boolean aBoolean, boolean calledBySuperuser)
|
||||||
|
{
|
||||||
|
return aBoolean == expectingTrue ? Optional.empty() : Optional.of(new ValidationViolation("Expecting true is " + expectingTrue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ValidationViolation> shouldFail(Boolean aBoolean, boolean calledBySuperUser)
|
||||||
|
{
|
||||||
|
return aBoolean == expectingTrue ? Optional.empty() : Optional.of(new ValidationViolation("Expecting true is " + expectingTrue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateParameters() throws ConfigurationException
|
||||||
|
{
|
||||||
|
if (!config.containsKey("expecting_true"))
|
||||||
|
throw new ConfigurationException("does not contain property 'expecting_true'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CustomGuardrailConfig getParameters()
|
||||||
|
{
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue