CEP-24 Password validation / generation

patch by Stefan Miklosovic; reviewed by Dinesh Joshi, Francisco Guerrero for CASSANDRA-17457
This commit is contained in:
Stefan Miklosovic 2024-06-10 17:28:39 +02:00
parent 12c2c6d645
commit d336dda112
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
48 changed files with 3127 additions and 41 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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>
---- ----

View File

@ -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.

View File

@ -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).

View File

@ -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

View File

@ -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>

View File

@ -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("")

View File

@ -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'])

View File

@ -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;

View File

@ -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

View File

@ -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());
} }

View File

@ -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();
} }

View File

@ -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
} }
/** /**

View File

@ -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'. " +

View File

@ -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"),

View File

@ -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.
*/ */

View File

@ -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;
}
} }

View File

@ -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));
}
} }

View File

@ -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

View File

@ -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);
}
} }

View File

@ -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)
{ {

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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)
{ {

View File

@ -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()
{ {

View File

@ -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();
} }

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.db.guardrails; package org.apache.cassandra.db.guardrails;
import java.util.Map;
import java.util.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);
} }

View File

@ -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
{
}
}

View File

@ -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
{
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);

View File

@ -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",

View File

@ -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.

View File

@ -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));
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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'");
}
}
}

View File

@ -0,0 +1,122 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.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;
}
}
}