You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by yc...@apache.org on 2023/07/29 03:10:18 UTC
[cassandra] branch trunk updated: CIDR filtering authorizer
This is an automated email from the ASF dual-hosted git repository.
ycai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push:
new 933137fb14 CIDR filtering authorizer
933137fb14 is described below
commit 933137fb14d4ebfe38d0d2c8e23b765aa1c89333
Author: Shailaja Koppu <s_...@apple.com>
AuthorDate: Tue Jun 13 17:18:29 2023 +0100
CIDR filtering authorizer
Patch by Shailaja Koppu, Yifan Cai; Reviewed by Dinesh Joshi, Yifan Cai for CASSANDRA-18592
Co-Authored-By: Yifan Cai <yc...@apache.org>
---
CHANGES.txt | 1 +
NEWS.txt | 1 +
conf/cassandra.yaml | 31 ++
doc/cql3/CQL.textile | 17 +-
.../examples/BNF/create_role_statement.bnf | 2 +
doc/modules/cassandra/examples/CQL/alter_role.cql | 2 +
doc/modules/cassandra/examples/CQL/create_role.cql | 2 +
.../cassandra/examples/CQL/create_user_role.cql | 3 +
.../cassandra/pages/developing/cql/security.adoc | 26 +-
.../cassandra/pages/managing/virtualtables.adoc | 29 ++
pylib/cqlshlib/cql3handling.py | 2 +
pylib/cqlshlib/test/test_cqlsh_completion.py | 3 +
src/antlr/Lexer.g | 1 +
src/antlr/Parser.g | 35 +-
.../cassandra/auth/AbstractCIDRAuthorizer.java | 92 ++++
.../cassandra/auth/AllowAllCIDRAuthorizer.java | 73 +++
src/java/org/apache/cassandra/auth/AuthConfig.java | 9 +
.../org/apache/cassandra/auth/AuthKeyspace.java | 34 +-
.../apache/cassandra/auth/AuthenticatedUser.java | 10 +
.../cassandra/auth/CIDRGroupsMappingCache.java | 135 ++++++
.../auth/CIDRGroupsMappingIntervalTree.java | 504 +++++++++++++++++++++
.../cassandra/auth/CIDRGroupsMappingLoader.java | 95 ++++
.../cassandra/auth/CIDRGroupsMappingManager.java | 251 ++++++++++
.../auth/CIDRGroupsMappingManagerMBean.java | 79 ++++
.../cassandra/auth/CIDRGroupsMappingTable.java | 80 ++++
.../org/apache/cassandra/auth/CIDRPermissions.java | 257 +++++++++++
.../cassandra/auth/CIDRPermissionsCache.java | 69 +++
.../cassandra/auth/CIDRPermissionsManager.java | 220 +++++++++
.../auth/CIDRPermissionsManagerMBean.java | 37 +-
.../cassandra/auth/CassandraCIDRAuthorizer.java | 157 +++++++
.../org/apache/cassandra/auth/ICIDRAuthorizer.java | 112 +++++
src/java/org/apache/cassandra/config/Config.java | 2 +
.../cassandra/config/DatabaseDescriptor.java | 69 +++
src/java/org/apache/cassandra/cql3/CIDR.java | 270 +++++++++++
.../cql3/statements/AlterRoleStatement.java | 22 +-
.../cql3/statements/CreateRoleStatement.java | 18 +-
.../cql3/statements/DropRoleStatement.java | 1 +
.../db/virtual/CIDRFilteringMetricsTable.java | 256 +++++++++++
.../db/virtual/CIDRFilteringMetricsTableMBean.java | 41 +-
.../cassandra/db/virtual/SystemViewsKeyspace.java | 1 +
.../cassandra/metrics/CIDRAuthorizerMetrics.java | 97 ++++
.../org/apache/cassandra/service/ClientState.java | 5 +
.../apache/cassandra/service/StorageService.java | 5 +-
.../org/apache/cassandra/tools/ListCIDRGroups.java | 52 +++
src/java/org/apache/cassandra/tools/NodeProbe.java | 126 ++++++
src/java/org/apache/cassandra/tools/NodeTool.java | 7 +
.../tools/nodetool/CIDRFilteringStats.java | 93 ++++
.../cassandra/tools/nodetool/DropCIDRGroup.java | 50 ++
.../tools/nodetool/GetCIDRGroupsOfIP.java | 50 ++
.../nodetool/InvalidateCIDRPermissionsCache.java | 56 +++
.../tools/nodetool/ReloadCIDRGroupsCache.java | 37 +-
.../cassandra/tools/nodetool/UpdateCIDRGroup.java | 57 +++
.../distributed/test/jmx/JMXGetterCheckTest.java | 3 +-
.../test/microbench/CIDRAuthorizerBench.java | 156 +++++++
.../test/microbench/LongestMatchingCIDRBench.java | 141 ++++++
test/unit/org/apache/cassandra/SchemaLoader.java | 6 +-
.../cassandra/auth/AllowAllCIDRAuthorizerTest.java | 172 +++++++
.../org/apache/cassandra/auth/AuthTestUtils.java | 151 ++++++
.../auth/CIDRGroupsMappingIntervalTreeTest.java | 232 ++++++++++
.../auth/CIDRGroupsMappingManagerTest.java | 164 +++++++
.../cassandra/auth/CIDRGroupsMappingTableTest.java | 220 +++++++++
.../CassandraCIDRAuthorizerEnforceModeTest.java | 300 ++++++++++++
.../CassandraCIDRAuthorizerMonitorModeTest.java | 138 ++++++
.../auth/CassandraNetworkAuthorizerTest.java | 38 +-
test/unit/org/apache/cassandra/auth/RolesTest.java | 3 +-
.../config/DatabaseDescriptorRefTest.java | 2 +
test/unit/org/apache/cassandra/cql3/CIDRTest.java | 194 ++++++++
test/unit/org/apache/cassandra/cql3/CQLTester.java | 2 +
.../cql3/statements/AddIdentityStatementTest.java | 3 +-
.../cql3/statements/AlterRoleStatementTest.java | 46 +-
.../cql3/statements/CreateRoleStatementTest.java | 39 +-
.../cql3/statements/CreateUserStatementTest.java | 14 +-
.../db/virtual/CIDRFilteringMetricsTableTest.java | 212 +++++++++
.../tools/nodetool/CIDRFilteringStatsTest.java | 136 ++++++
.../tools/nodetool/DropCIDRGroupTest.java | 142 ++++++
.../tools/nodetool/GetCIDRGroupsOfIPTest.java | 126 ++++++
.../InvalidateCIDRPermissionsCacheTest.java | 168 +++++++
.../tools/nodetool/ListCIDRGroupTest.java | 129 ++++++
.../tools/nodetool/ReloadCIDRGroupsCacheTest.java | 135 ++++++
.../tools/nodetool/UpdateCIDRGroupTest.java | 166 +++++++
.../cassandra/tools/nodetool/VerifyTest.java | 3 +-
81 files changed, 6787 insertions(+), 138 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 7a60b6ae95..ef4749b12c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.0
+ * CIDR filtering authorizer (CASSANDRA-18592)
* Remove 3.x from the versions checked for prepared statement behaviour (CASSANDRA-18695)
* Add vector similarity functions (CASSANDRA-18640)
* Lift MessagingService.minimum_version to 40 (CASSANDRA-18314)
diff --git a/NEWS.txt b/NEWS.txt
index 378ae6e93c..9b8441f857 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -71,6 +71,7 @@ using the provided 'sstableupgrade' tool.
New features
------------
+ - Added a new authorizer, CIDR authorizer, to restrict user access based on CIDR groups.
- Pluggable crypto providers were made possible via `crypto_provider` section in cassandra.yaml. The default provider is
Amazon Corretto Crypto Provider and it is installed automatically upon node's start. Only x86_64 and aarch64 architectures are supported now.
Please consult upgrade section to know more details when upgrading from older Cassandra versions.
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 8783f53252..26d3579f03 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -189,6 +189,37 @@ role_manager: CassandraRoleManager
# increase system_auth keyspace replication factor if you use this authorizer.
network_authorizer: AllowAllNetworkAuthorizer
+# CIDR authorization backend, implementing ICIDRAuthorizer; used to restrict user
+# access from certain CIDRs
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllCIDRAuthorizer,
+# CassandraCIDRAuthorizer}.
+# - AllowAllCIDRAuthorizer allows access from any CIDR to any user - set it to disable CIDR authorization.
+# - CassandraCIDRAuthorizer stores user's CIDR permissions in system_auth.cidr_permissions table. Please
+# increase system_auth keyspace replication factor if you use this authorizer, otherwise any changes to
+# system_auth tables being used by this feature may be lost when a host goes down.
+cidr_authorizer:
+ class_name: AllowAllCIDRAuthorizer
+ # Below parameters are used only when CIDR authorizer is enabled
+ # parameters:
+ # CIDR authorizer when enabled, i.e, CassandraCIDRAuthorizer, is applicable for non-superusers only by default.
+ # Set this setting to true, to enable CIDR authorization for superusers as well.
+ # Note: CIDR checks cannot be performed for JMX calls
+ # cidr_checks_for_superusers: true
+
+ # CIDR authorizer when enabled, supports MONITOR and ENFORCE modes. Default mode is MONITOR
+ # In MONITOR mode, CIDR checks are NOT enforced. Instead, CIDR groups of users accesses are logged using
+ # nospamlogger. A warning message would be logged if a user accesses from unauthorized CIDR group (but access won't
+ # be rejected). An info message would be logged otherwise.
+ # In ENFORCE mode, CIDR checks are enforced, i.e, users accesses would be rejected if attempted from unauthorized
+ # CIDR groups.
+ # cidr_authorizer_mode: MONITOR
+
+ # Refresh interval for CIDR groups cache, this value is considered in minutes
+ # cidr_groups_cache_refresh_interval: 5
+
+ # Maximum number of entries an IP to CIDR groups cache can accommodate
+ # ip_cache_max_size: 100
+
# Depending on the auth strategy of the cluster, it can be beneficial to iterate
# from root to table (root -> ks -> table) instead of table to root (table -> ks -> root).
# As the auth entries are whitelisting, once a permission is found you know it to be
diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile
index 4f6a4d7338..88918f87d9 100644
--- a/doc/cql3/CQL.textile
+++ b/doc/cql3/CQL.textile
@@ -1283,6 +1283,10 @@ bc(syntax)..
| LOGIN = <boolean>
| SUPERUSER = <boolean>
| OPTIONS = <map_literal>
+ | ACCESS TO DATACENTERS { dcPermission (, dcPermission)* }
+ | ACCESS TO ALL DATACENTERS
+ | ACCESS FROM CIDRS { cidrPermission (, cidrPermission)* }
+ | ACCESS FROM ALL CIDRS
p.
__Sample:__
@@ -1292,6 +1296,9 @@ CREATE ROLE new_role;
CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true;
CREATE ROLE bob WITH PASSWORD = 'password_b' AND LOGIN = true AND SUPERUSER = true;
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 hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
+
By default roles do not possess @LOGIN@ privileges or @SUPERUSER@ status.
@@ -1299,7 +1306,7 @@ By default roles do not possess @LOGIN@ privileges or @SUPERUSER@ status.
If a role has the @LOGIN@ privilege, clients may identify as that role when connecting. For the duration of that connection, the client will acquire any roles and privileges granted to that role.
-Only a client with with the @CREATE@ permission on the database roles resource may issue @CREATE ROLE@ requests (see the "relevant section":#permissions below), unless the client is a @SUPERUSER@. Role management in Cassandra is pluggable and custom implementations may support only a subset of the listed options.
+Only a client with the @CREATE@ permission on the database roles resource may issue @CREATE ROLE@ requests (see the "relevant section":#permissions below), unless the client is a @SUPERUSER@. Role management in Cassandra is pluggable and custom implementations may support only a subset of the listed options.
Role names should be quoted if they contain non-alphanumeric characters.
@@ -1327,12 +1334,18 @@ bc(syntax)..
| LOGIN = <boolean>
| SUPERUSER = <boolean>
| OPTIONS = <map_literal>
+ | ACCESS TO DATACENTERS { dcPermission (, dcPermission)* }
+ | ACCESS TO ALL DATACENTERS
+ | ACCESS FROM CIDRS { cidrPermission (, cidrPermission)* }
+ | ACCESS FROM ALL CIDRS
p.
__Sample:__
bc(sample).
ALTER ROLE 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 hob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1' };
Conditions on executing @ALTER ROLE@ statements:
@@ -2493,6 +2506,7 @@ CQL distinguishes between _reserved_ and _non-reserved_ keywords. Reserved keywo
| @BY@ | yes |
| @CALLED@ | no |
| @CAST@ | no |
+| @CIDRS@ | no |
| @CLUSTERING@ | no |
| @COLUMNFAMILY@ | yes |
| @COMPACT@ | no |
@@ -2647,6 +2661,7 @@ h3. 3.4.6
* Allow to grant permission for all tables in a keyspace (see "CASSANDRA-17027":https://issues.apache.org/jira/browse/CASSANDRA-17027).
* Allow to use pure monotonic functions on the last attribute of the @GROUP BY@ clause (see "CASSANDRA-11871":https://issues.apache.org/jira/browse/CASSANDRA-11871).
* Add floor function to allow grouping by time range (see "CASSANDRA-11871":https://issues.apache.org/jira/browse/CASSANDRA-11871).
+* Add support for adding and updating CIDR permissions using @CREATE ROLE@ and @ALTER ROLE@ statements
h3. 3.4.5
diff --git a/doc/modules/cassandra/examples/BNF/create_role_statement.bnf b/doc/modules/cassandra/examples/BNF/create_role_statement.bnf
index 4236cc6ee2..8e4e0cda80 100644
--- a/doc/modules/cassandra/examples/BNF/create_role_statement.bnf
+++ b/doc/modules/cassandra/examples/BNF/create_role_statement.bnf
@@ -8,3 +8,5 @@ role_option ::= PASSWORD '=' string
| OPTIONS '=' map_literal
| ACCESS TO DATACENTERS set_literal
| ACCESS TO ALL DATACENTERS
+ | ACCESS FROM CIDRS set_literal
+ | ACCESS FROM ALL CIDRS
diff --git a/doc/modules/cassandra/examples/CQL/alter_role.cql b/doc/modules/cassandra/examples/CQL/alter_role.cql
index 1e858aea23..516262152c 100644
--- a/doc/modules/cassandra/examples/CQL/alter_role.cql
+++ b/doc/modules/cassandra/examples/CQL/alter_role.cql
@@ -1,2 +1,4 @@
ALTER ROLE bob WITH PASSWORD = 'PASSWORD_B' AND SUPERUSER = false;
ALTER ROLE bob WITH HASHED PASSWORD = '$2a$10$JSJEMFm6GeaW9XxT5JIheuEtPvat6i7uKbnTcxX3c1wshIIsGyUtG' AND SUPERUSER = false;
+ALTER ROLE 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' };
diff --git a/doc/modules/cassandra/examples/CQL/create_role.cql b/doc/modules/cassandra/examples/CQL/create_role.cql
index 2ceee54d62..593556039e 100644
--- a/doc/modules/cassandra/examples/CQL/create_role.cql
+++ b/doc/modules/cassandra/examples/CQL/create_role.cql
@@ -5,3 +5,5 @@ 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 alice WITH PASSWORD = 'password_a' AND LOGIN = true AND ACCESS TO DATACENTERS {'DC1', 'DC3'};
CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true AND ACCESS TO ALL DATACENTERS;
+CREATE ROLE bob WITH LOGIN = true and PASSWORD = 'password_d' AND ACCESS FROM CIDRS { 'region1', 'region2' };
+CREATE ROLE hob WITH LOGIN = true and PASSWORD = 'password_c' AND ACCESS FROM ALL CIDRS;
diff --git a/doc/modules/cassandra/examples/CQL/create_user_role.cql b/doc/modules/cassandra/examples/CQL/create_user_role.cql
index 810f76ca9c..ff44c75a51 100644
--- a/doc/modules/cassandra/examples/CQL/create_user_role.cql
+++ b/doc/modules/cassandra/examples/CQL/create_user_role.cql
@@ -12,3 +12,6 @@ CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true;
CREATE USER alice WITH PASSWORD 'password_a';
CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true;
+
+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' };
diff --git a/doc/modules/cassandra/pages/developing/cql/security.adoc b/doc/modules/cassandra/pages/developing/cql/security.adoc
index 166eb870a3..1c9a89ff30 100644
--- a/doc/modules/cassandra/pages/developing/cql/security.adoc
+++ b/doc/modules/cassandra/pages/developing/cql/security.adoc
@@ -69,6 +69,18 @@ specifiying datacenters implicitly grants access to all datacenters. The
clause `ACCESS TO ALL DATACENTERS` can be used for explicitness, but
there's no functional difference.
+==== Restricting connections from specific CIDR groups
+
+If a `cidr_authorizer` has been configured, you can restrict roles to login
+only from specific regions, aka CIDR groups, with the `ACCESS FROM CIDRS` clause
+followed by a set literal of CIDR groups the user can access from. Not
+specifying CIDR groups implicitly grants access from all CIDR groups. The
+clause `ACCESS FROM ALL CIDRS` can be used for explicitness, but there's no
+functional difference. This clause can be also be used to remove any CIDR
+groups restrictions. Valid CIDR groups should be used with `ACCESS FROM CIDRS`
+clause. `nodetool list-cidrgroups` command can be used to see available CIDR groups
+in the Cluster.
+
==== Creating a role conditionally
Attempting to create an existing role results in an invalid query
@@ -108,7 +120,19 @@ followed by a set literal of datacenters the user can access. To remove
any data center restrictions, use the `ACCESS TO ALL DATACENTERS`
clause.
-Conditions on executing `ALTER ROLE` statements:
+==== Restricting connections from specific CIDR groups
+
+If a `cidr_authorizer` has been configured, you can restrict roles to login
+only from specific regions, aka CIDR groups, with the `ACCESS FROM CIDRS` clause
+followed by a set literal of CIDR groups the user can access from. Not
+specifying CIDR groups implicitly grants access from all CIDR groups. The
+clause `ACCESS FROM ALL CIDRS` can be used for explicitness, but there's no
+functional difference. This clause can be also be used to remove any CIDR
+groups restrictions. Valid CIDR groups should be used with `ACCESS FROM CIDRS`
+clause. `nodetool list-cidrgroups` command can be used to see available CIDR groups
+in the Cluster.
+
+==== Conditions on executing `ALTER ROLE` statements:
* a client must have `SUPERUSER` status to alter the `SUPERUSER` status
of another role
diff --git a/doc/modules/cassandra/pages/managing/virtualtables.adoc b/doc/modules/cassandra/pages/managing/virtualtables.adoc
index 22262c1b14..f43f4d7c00 100644
--- a/doc/modules/cassandra/pages/managing/virtualtables.adoc
+++ b/doc/modules/cassandra/pages/managing/virtualtables.adoc
@@ -66,6 +66,10 @@ The following table describes the virtual tables:
|caches |Displays the general cache information including cache name, capacity_bytes, entry_count, hit_count, hit_ratio double,
recent_hit_rate_per_second, recent_request_rate_per_second, request_count, and size_bytes.
+|cidr_filtering_metrics_counts |Counts metrics specific to CIDR filtering.
+
+|cidr_filtering_metrics_latencies |Latencies metrics specific to CIDR filtering.
+
|clients |Lists information about all connected clients.
|coordinator_read_latency |Records counts, keyspace_name, table_name, max, median, and per_second for coordinator reads.
@@ -244,6 +248,31 @@ counters | 26214400 | 0 | 0 | NaN |
(4 rows)
....
+
+=== CIDR filtering metrics Virtual Tables
+The `cidr_filtering_metrics_counts` virtual table lists counts metrics specific to CIDR filtering. A query on `cidr_filtering_metrics_counts` virtual table lists metrics similar to below.
+
+....
+cqlsh> select * from system_views.cidr_filtering_metrics_counts;
+ name | value
+--------------------------------------------------------+-------
+ CIDR groups cache reload count | 2
+ Number of CIDR accesses accepted from CIDR group - aws | 15
+ Number of CIDR accesses accepted from CIDR group - gcp | 30
+ Number of CIDR accesses rejected from CIDR group - gcp | 6
+....
+
+The `cidr_filtering_metrics_latencies` virtual table lists latencies metrics specific to CIDR filtering. A query on `cidr_filtering_metrics_latencies` virtual table lists below metrics.
+
+....
+cqlsh> select * from system_views.cidr_filtering_metrics_latencies;
+ name | max | p50th | p95th | p999th | p99th
+---------------------------------------------+-------+-------+-------+--------+-------
+ CIDR checks latency (ns) | 24601 | 1 | 11864 | 24601 | 24601
+ CIDR groups cache reload latency (ns) | 42510 | 42510 | 42510 | 42510 | 42510
+ Lookup IP in CIDR groups cache latency (ns) | 1 | 1 | 1 | 1 | 1
+....
+
=== CQL metrics Virtual Table
The `cql_metrics` virtual table lists metrics specific to CQL prepared statement caching. A query on `cql_metrics` virtual table lists below metrics.
diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py
index 7ff252be32..4c81926ce4 100644
--- a/pylib/cqlshlib/cql3handling.py
+++ b/pylib/cqlshlib/cql3handling.py
@@ -1498,6 +1498,8 @@ syntax_rules += r'''
| "LOGIN" "=" <boolean>
| "ACCESS" "TO" "DATACENTERS" <setLiteral>
| "ACCESS" "TO" "ALL" "DATACENTERS"
+ | "ACCESS" "FROM" "CIDRS" <setLiteral>
+ | "ACCESS" "FROM" "ALL" "CIDRS"
;
<dropRoleStatement> ::= "DROP" "ROLE" ("IF" "EXISTS")? <rolename>
diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py
index b0f0b87a1c..db59297a97 100644
--- a/pylib/cqlshlib/test/test_cqlsh_completion.py
+++ b/pylib/cqlshlib/test/test_cqlsh_completion.py
@@ -998,12 +998,15 @@ class TestCqlshCompletion(CqlshCompletionCase):
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 ALL ', immediate='DATACENTERS ')
+ self.trycompletions('CREATE ROLE foo WITH ACCESS FROM ', choices=['ALL', 'CIDRS'])
+ self.trycompletions('CREATE ROLE foo WITH ACCESS FROM ALL ', immediate='CIDRS ')
def test_complete_in_alter_role(self):
self.trycompletions('ALTER ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
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 ACCESS TO ', choices=['ALL', 'DATACENTERS'])
+ self.trycompletions('ALTER ROLE foo WITH ACCESS FROM ', choices=['ALL', 'CIDRS'])
def test_complete_in_drop_role(self):
self.trycompletions('DROP ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g
index c75523ed74..77e1202e4c 100644
--- a/src/antlr/Lexer.g
+++ b/src/antlr/Lexer.g
@@ -156,6 +156,7 @@ K_NOLOGIN: N O L O G I N;
K_OPTIONS: O P T I O N S;
K_ACCESS: A C C E S S;
K_DATACENTERS: D A T A C E N T E R S;
+K_CIDRS: C I D R S;
K_IDENTITY: I D E N T I T Y;
K_CLUSTERING: C L U S T E R I N G;
diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g
index 0d2150481d..a2d43b7392 100644
--- a/src/antlr/Parser.g
+++ b/src/antlr/Parser.g
@@ -1204,7 +1204,7 @@ createUserStatement returns [CreateRoleStatement stmt]
{
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
}
- $stmt = new CreateRoleStatement(name, opts, DCPermissions.all(), ifNotExists); }
+ $stmt = new CreateRoleStatement(name, opts, DCPermissions.all(), CIDRPermissions.all(), ifNotExists); }
;
/**
@@ -1225,7 +1225,7 @@ alterUserStatement returns [AlterRoleStatement stmt]
{
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
}
- $stmt = new AlterRoleStatement(name, opts, null, ifExists);
+ $stmt = new AlterRoleStatement(name, opts, null, null, ifExists);
}
;
@@ -1277,15 +1277,20 @@ listUsersStatement returns [ListRolesStatement stmt]
* SUPERUSER = (true|false)
* LOGIN = (true|false)
* OPTIONS = { 'k1':'v1', 'k2':'v2'}
+ * ACCESS TO ALL DATACENTERS
+ * ACCESS TO DATACENTERS { dcPermission (, dcPermission)* }
+ * ACCESS FROM ALL CIDRS
+ * ACCESS FROM CIDRS { cidrPermission (, cidrPermission)* }
*/
createRoleStatement returns [CreateRoleStatement stmt]
@init {
RoleOptions opts = new RoleOptions();
DCPermissions.Builder dcperms = DCPermissions.builder();
+ CIDRPermissions.Builder cidrperms = CIDRPermissions.builder();
boolean ifNotExists = false;
}
: K_CREATE K_ROLE (K_IF K_NOT K_EXISTS { ifNotExists = true; })? name=userOrRoleName
- ( K_WITH roleOptions[opts, dcperms] )?
+ ( K_WITH roleOptions[opts, dcperms, cidrperms] )?
{
// set defaults if they weren't explictly supplied
if (!opts.getLogin().isPresent())
@@ -1300,7 +1305,7 @@ createRoleStatement returns [CreateRoleStatement stmt]
{
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
}
- $stmt = new CreateRoleStatement(name, opts, dcperms.build(), ifNotExists);
+ $stmt = new CreateRoleStatement(name, opts, dcperms.build(), cidrperms.build(), ifNotExists);
}
;
@@ -1312,21 +1317,26 @@ createRoleStatement returns [CreateRoleStatement stmt]
* SUPERUSER = (true|false)
* LOGIN = (true|false)
* OPTIONS = { 'k1':'v1', 'k2':'v2'}
+ * ACCESS TO ALL DATACENTERS
+ * ACCESS TO DATACENTERS { dcPermission (, dcPermission)* }
+ * ACCESS FROM ALL CIDRS
+ * ACCESS FROM CIDRS { cidrPermission (, cidrPermission)* }
*/
alterRoleStatement returns [AlterRoleStatement stmt]
@init {
RoleOptions opts = new RoleOptions();
DCPermissions.Builder dcperms = DCPermissions.builder();
+ CIDRPermissions.Builder cidrperms = CIDRPermissions.builder();
boolean ifExists = false;
}
: K_ALTER K_ROLE (K_IF K_EXISTS { ifExists = true; })? name=userOrRoleName
- ( K_WITH roleOptions[opts, dcperms] )?
+ ( K_WITH roleOptions[opts, dcperms, cidrperms] )?
{
if (opts.getPassword().isPresent() && opts.getHashedPassword().isPresent())
{
throw new SyntaxException("Options 'password' and 'hashed password' are mutually exclusive");
}
- $stmt = new AlterRoleStatement(name, opts, dcperms.isModified() ? dcperms.build() : null, ifExists);
+ $stmt = new AlterRoleStatement(name, opts, dcperms.isModified() ? dcperms.build() : null, cidrperms.isModified() ? cidrperms.build() : null, ifExists);
}
;
@@ -1355,11 +1365,11 @@ listRolesStatement returns [ListRolesStatement stmt]
{ $stmt = new ListRolesStatement(grantee, recursive); }
;
-roleOptions[RoleOptions opts, DCPermissions.Builder dcperms]
- : roleOption[opts, dcperms] (K_AND roleOption[opts, dcperms])*
+roleOptions[RoleOptions opts, DCPermissions.Builder dcperms, CIDRPermissions.Builder cidrperms]
+ : roleOption[opts, dcperms, cidrperms] (K_AND roleOption[opts, dcperms, cidrperms])*
;
-roleOption[RoleOptions opts, DCPermissions.Builder dcperms]
+roleOption[RoleOptions opts, DCPermissions.Builder dcperms, CIDRPermissions.Builder cidrperms]
: 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_OPTIONS '=' m=fullMapLiteral { opts.setOption(IRoleManager.Option.OPTIONS, convertPropertyMap(m)); }
@@ -1367,12 +1377,18 @@ roleOption[RoleOptions opts, DCPermissions.Builder dcperms]
| K_LOGIN '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.LOGIN, Boolean.valueOf($b.text)); }
| K_ACCESS K_TO K_ALL K_DATACENTERS { dcperms.all(); }
| K_ACCESS K_TO K_DATACENTERS '{' dcPermission[dcperms] (',' dcPermission[dcperms])* '}'
+ | K_ACCESS K_FROM K_ALL K_CIDRS { cidrperms.all(); }
+ | K_ACCESS K_FROM K_CIDRS '{' cidrPermission[cidrperms] (',' cidrPermission[cidrperms])* '}'
;
dcPermission[DCPermissions.Builder builder]
: dc=STRING_LITERAL { builder.add($dc.text); }
;
+cidrPermission[CIDRPermissions.Builder builder]
+ : cidr=STRING_LITERAL { builder.add($cidr.text); }
+ ;
+
// for backwards compatibility in CREATE/ALTER USER, this has no '='
userPassword[RoleOptions opts]
: K_PASSWORD v=STRING_LITERAL { opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
@@ -1987,6 +2003,7 @@ basic_unreserved_keyword returns [String str]
| K_PARTITION
| K_GROUP
| K_DATACENTERS
+ | K_CIDRS
| K_ACCESS
| K_DEFAULT
| K_MBEAN
diff --git a/src/java/org/apache/cassandra/auth/AbstractCIDRAuthorizer.java b/src/java/org/apache/cassandra/auth/AbstractCIDRAuthorizer.java
new file mode 100644
index 0000000000..a2d591318e
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/AbstractCIDRAuthorizer.java
@@ -0,0 +1,92 @@
+/*
+ * 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.auth;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
+
+/**
+ * Abstract CIDR authorizer, contains code common to all implementations of ICIDRAuthorizer
+ */
+public abstract class AbstractCIDRAuthorizer implements ICIDRAuthorizer
+{
+ protected static CIDRPermissionsManager cidrPermissionsManager;
+ protected static CIDRGroupsMappingManager cidrGroupsMappingManager;
+
+ protected static CIDRAuthorizerMetrics cidrAuthorizerMetrics;
+
+ @VisibleForTesting
+ void createManagers()
+ {
+ // CIDRPermissionsManager provides functionality to retrieve or update CIDR permissions for a role
+ cidrPermissionsManager = new CIDRPermissionsManager();
+
+ // CIDRGroupsMappingManager provides functionality to retrieve or update CIDR groups mapping table
+ cidrGroupsMappingManager = new CIDRGroupsMappingManager();
+ }
+
+ protected void commonSetup()
+ {
+ createManagers();
+
+ cidrPermissionsManager.setup();
+ cidrGroupsMappingManager.setup();
+
+ cidrAuthorizerMetrics = new CIDRAuthorizerMetrics();
+ }
+
+ @Override
+ public CIDRGroupsMappingManager getCidrGroupsMappingManager()
+ {
+ return cidrGroupsMappingManager;
+ }
+
+ @Override
+ public CIDRAuthorizerMetrics getCidrAuthorizerMetrics() { return cidrAuthorizerMetrics; }
+
+ @Override
+ public boolean requireAuthorization()
+ {
+ return true;
+ }
+
+ @Override
+ public void setCidrGroupsForRole(RoleResource role, CIDRPermissions cidrPermissions)
+ {
+ // Allow 'create role' and 'alter role' CQL commands with ACCESS FROM CIDRS clause in both cases,
+ // when feature is enabled and also disabled
+ cidrPermissionsManager.setCidrGroupsForRole(role, cidrPermissions);
+ }
+
+ @Override
+ public void dropCidrPermissionsForRole(RoleResource role)
+ {
+ // Allow dropping of CIDR permissions for roles in both cases,
+ // when feature is enabled and also disabled
+ cidrPermissionsManager.drop(role);
+ }
+
+ @Override
+ public void validateConfiguration() throws ConfigurationException
+ {
+ // do nothing
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/AllowAllCIDRAuthorizer.java b/src/java/org/apache/cassandra/auth/AllowAllCIDRAuthorizer.java
new file mode 100644
index 0000000000..394b798bfb
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/AllowAllCIDRAuthorizer.java
@@ -0,0 +1,73 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.util.Set;
+
+import org.apache.cassandra.exceptions.InvalidRequestException;
+
+/**
+ * AllowAllCIDRAuthorizer allows any user to access from any CIDR
+ * i.e, disables CIDR authorization
+ */
+public class AllowAllCIDRAuthorizer extends AbstractCIDRAuthorizer
+{
+ public void setup()
+ {
+ commonSetup();
+ }
+
+ @Override
+ public void initCaches()
+ {
+ // Caches not created when CIDR authorization is disabled
+ }
+
+ @Override
+ public boolean requireAuthorization()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean invalidateCidrPermissionsCache(String roleName)
+ {
+ throw new InvalidRequestException("Invalidate CIDR permissions cache operation not supported by " + getClass().getSimpleName());
+ }
+
+ @Override
+ public void loadCidrGroupsCache()
+ {
+ throw new InvalidRequestException("Load CIDR groups cache operation not supported by " + getClass().getSimpleName());
+ }
+
+ @Override
+ public Set<String> lookupCidrGroupsForIp(InetAddress ip)
+ {
+ throw new InvalidRequestException("'Get CIDR groups for IP' operation not supported by " + getClass().getSimpleName());
+ }
+
+ @Override
+ public boolean hasAccessFromIp(RoleResource role, InetAddress ipAddress)
+ {
+ // Allow all accesses
+ return true;
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/AuthConfig.java b/src/java/org/apache/cassandra/auth/AuthConfig.java
index 5cecd6f130..786459d1ce 100644
--- a/src/java/org/apache/cassandra/auth/AuthConfig.java
+++ b/src/java/org/apache/cassandra/auth/AuthConfig.java
@@ -115,6 +115,14 @@ public final class AuthConfig
throw new ConfigurationException(conf.network_authorizer + " can't be used with " + conf.authenticator.class_name, false);
}
+ // cidr authorizer
+ ICIDRAuthorizer cidrAuthorizer = ICIDRAuthorizer.newCIDRAuthorizer(conf.cidr_authorizer);
+ DatabaseDescriptor.setCIDRAuthorizer(cidrAuthorizer);
+ if (cidrAuthorizer.requireAuthorization() && !authenticator.requireAuthentication())
+ {
+ throw new ConfigurationException(conf.cidr_authorizer + " can't be used with " + conf.authenticator, false);
+ }
+
// Validate at last to have authenticator, authorizer, role-manager and internode-auth setup
// in case these rely on each other.
@@ -122,6 +130,7 @@ public final class AuthConfig
authorizer.validateConfiguration();
roleManager.validateConfiguration();
networkAuthorizer.validateConfiguration();
+ cidrAuthorizer.validateConfiguration();
DatabaseDescriptor.getInternodeAuthenticator().validateConfiguration();
}
}
diff --git a/src/java/org/apache/cassandra/auth/AuthKeyspace.java b/src/java/org/apache/cassandra/auth/AuthKeyspace.java
index b1616e07fa..b495c90419 100644
--- a/src/java/org/apache/cassandra/auth/AuthKeyspace.java
+++ b/src/java/org/apache/cassandra/auth/AuthKeyspace.java
@@ -58,8 +58,13 @@ public final class AuthKeyspace
public static final String ROLE_PERMISSIONS = "role_permissions";
public static final String RESOURCE_ROLE_INDEX = "resource_role_permissons_index";
public static final String NETWORK_PERMISSIONS = "network_permissions";
+ public static final String CIDR_PERMISSIONS = "cidr_permissions";
+ public static final String CIDR_GROUPS = "cidr_groups";
public static final String IDENTITY_TO_ROLES = "identity_to_role";
- public static final Set<String> TABLE_NAMES = ImmutableSet.of(ROLES, ROLE_MEMBERS, ROLE_PERMISSIONS, RESOURCE_ROLE_INDEX, NETWORK_PERMISSIONS, IDENTITY_TO_ROLES);
+ public static final Set<String> TABLE_NAMES = ImmutableSet.of(ROLES, ROLE_MEMBERS, ROLE_PERMISSIONS,
+ RESOURCE_ROLE_INDEX, NETWORK_PERMISSIONS,
+ CIDR_PERMISSIONS, CIDR_GROUPS,
+ IDENTITY_TO_ROLES);
public static final long SUPERUSER_SETUP_DELAY = SUPERUSER_SETUP_DELAY_MS.getLong();
@@ -116,6 +121,28 @@ public final class AuthKeyspace
+ "dcs frozen<set<text>>, "
+ "PRIMARY KEY(role))");
+ public static final String CIDR_PERMISSIONS_TBL_ROLE_COL_NAME = "role";
+ public static final String CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME = "cidr_groups";
+ private static final TableMetadata CIDRPermissions =
+ parse(CIDR_PERMISSIONS,
+ "user cidr permissions",
+ "CREATE TABLE %s ("
+ + CIDR_PERMISSIONS_TBL_ROLE_COL_NAME + " text, "
+ + CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME + " frozen<set<text>>, "
+ + "PRIMARY KEY(" + CIDR_PERMISSIONS_TBL_ROLE_COL_NAME + "))"
+ );
+
+ public static final String CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME = "cidr_group";
+ public static final String CIDR_GROUPS_TBL_CIDRS_COL_NAME = "cidrs";
+ private static final TableMetadata CIDRGroups =
+ parse(CIDR_GROUPS,
+ "cidr groups to cidrs mapping",
+ "CREATE TABLE %s ("
+ + CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME + " text, "
+ + CIDR_GROUPS_TBL_CIDRS_COL_NAME + " frozen<set<tuple<inet, smallint>>>, "
+ + "PRIMARY KEY(" + CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME + "))"
+ );
+
private static TableMetadata parse(String name, String description, String cql)
{
return CreateTableStatement.parse(format(cql, name), SchemaConstants.AUTH_KEYSPACE_NAME)
@@ -129,6 +156,9 @@ public final class AuthKeyspace
{
return KeyspaceMetadata.create(SchemaConstants.AUTH_KEYSPACE_NAME,
KeyspaceParams.simple(Math.max(DEFAULT_RF, DatabaseDescriptor.getDefaultKeyspaceRF())),
- Tables.of(Roles, RoleMembers, RolePermissions, ResourceRoleIndex, NetworkPermissions, IdentityToRoles));
+ Tables.of(Roles, RoleMembers, RolePermissions,
+ ResourceRoleIndex, NetworkPermissions,
+ CIDRPermissions, CIDRGroups,
+ IdentityToRoles));
}
}
diff --git a/src/java/org/apache/cassandra/auth/AuthenticatedUser.java b/src/java/org/apache/cassandra/auth/AuthenticatedUser.java
index 620a1d2ded..6b31110e95 100644
--- a/src/java/org/apache/cassandra/auth/AuthenticatedUser.java
+++ b/src/java/org/apache/cassandra/auth/AuthenticatedUser.java
@@ -17,6 +17,7 @@
*/
package org.apache.cassandra.auth;
+import java.net.InetSocketAddress;
import java.util.Set;
import com.google.common.base.Objects;
@@ -41,11 +42,15 @@ public class AuthenticatedUser
public static final PermissionsCache permissionsCache = new PermissionsCache(DatabaseDescriptor.getAuthorizer());
public static final NetworkPermissionsCache networkPermissionsCache = new NetworkPermissionsCache(DatabaseDescriptor.getNetworkAuthorizer());
+ private static final ICIDRAuthorizer cidrAuthorizer = DatabaseDescriptor.getCIDRAuthorizer();
+
/** Use {@link AuthCacheService#initializeAndRegisterCaches} rather than calling this directly */
public static void init()
{
AuthCacheService.instance.register(permissionsCache);
AuthCacheService.instance.register(networkPermissionsCache);
+
+ cidrAuthorizer.initCaches();
}
private final String name;
@@ -146,6 +151,11 @@ public class AuthenticatedUser
return networkPermissionsCache.get(this.getPrimaryRole()).canAccess(Datacenters.thisDatacenter());
}
+ public boolean hasAccessFromIp(InetSocketAddress remoteAddress)
+ {
+ return cidrAuthorizer.hasAccessFromIp(role, remoteAddress.getAddress());
+ }
+
@Override
public String toString()
{
diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingCache.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingCache.java
new file mode 100644
index 0000000000..5f7546459f
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingCache.java
@@ -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.auth;
+
+import java.net.InetAddress;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Preconditions;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.LoadingCache;
+import org.apache.cassandra.concurrent.ImmediateExecutor;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
+import org.apache.cassandra.utils.MonotonicClock;
+
+/**
+ * CIDR groups mapping cache. Periodically reloads the cache with the latest entries
+ * in the table {@link AuthKeyspace#CIDR_GROUPS}
+ */
+public class CIDRGroupsMappingCache
+{
+ // References to the latest mapping, changes everytime cache is reloaded
+ private volatile CIDRGroupsMappingLoader currentCidrGroupsMappingLoader = null;
+
+ // IP to CIDR groups cache, to reuse CIDR groups lookup result for an IP
+ private LoadingCache<InetAddress, Set<String>> ipToCidrGroupsCache;
+
+ private volatile boolean cacheInitialized = false;
+
+ private CIDRGroupsMappingManager cidrGroupsMappingManager;
+ private final CIDRAuthorizerMetrics cidrAuthorizerMetrics;
+
+ public CIDRGroupsMappingCache(CIDRGroupsMappingManager cidrGroupsMappingManager,
+ CIDRAuthorizerMetrics cidrAuthorizerMetrics)
+ {
+ this.cidrGroupsMappingManager = cidrGroupsMappingManager;
+ this.cidrAuthorizerMetrics = cidrAuthorizerMetrics;
+
+ // IP to CIDR groups cache
+ ipToCidrGroupsCache = Caffeine.newBuilder()
+ .maximumSize(DatabaseDescriptor.getIpCacheMaxSize())
+ .executor(ImmediateExecutor.INSTANCE)
+ .build(this::lookupCidrGroupsCacheForIp);
+
+ // Interval to reload the cache periodically
+ int cidrGroupsCacheRefreshInterval = DatabaseDescriptor.getCidrGroupsCacheRefreshInterval();
+ // Schedule a task to reload cache periodically
+ ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(this::reloadCidrGroupsCache,
+ cidrGroupsCacheRefreshInterval,
+ cidrGroupsCacheRefreshInterval,
+ TimeUnit.MINUTES);
+ }
+
+ /**
+ * Builds CIDR groups cache from entries in the table {@link AuthKeyspace#CIDR_GROUPS}
+ * and points to the latest cache
+ */
+ private void loadCidrGroupsCacheInternal()
+ {
+ long startTimeNanos = MonotonicClock.Global.approxTime.now();
+
+ currentCidrGroupsMappingLoader = new CIDRGroupsMappingLoader(this.cidrGroupsMappingManager);
+ // Clear IP to CIDR groups cache entries as we reloaded CIDR groups cache
+ ipToCidrGroupsCache.invalidateAll();
+
+ cidrAuthorizerMetrics.cacheReloadCount.inc();
+ cidrAuthorizerMetrics.cacheReloadLatency.update(MonotonicClock.Global.approxTime.now() - startTimeNanos,
+ TimeUnit.NANOSECONDS);
+ }
+
+ /**
+ * Reload CIDR groups cache with the latest state of the table {@link AuthKeyspace#CIDR_GROUPS}
+ */
+ private void reloadCidrGroupsCache()
+ {
+ // Wait until CIDR authorizer calls init cache during the setup
+ // Otherwise scheduled task may trigger this before startup is complete
+ // and fails to find system tables
+ if (cacheInitialized)
+ loadCidrGroupsCacheInternal();
+ }
+
+ /**
+ * Builds CIDR groups cache the first time. Should be called before
+ * auth starts sending lookup IP requests
+ */
+ public void loadCidrGroupsCache()
+ {
+ loadCidrGroupsCacheInternal();
+ cacheInitialized = true;
+ }
+
+ private Set<String> lookupCidrGroupsCacheForIp(InetAddress ipAddr)
+ {
+ long startTimeNanos = MonotonicClock.Global.approxTime.now();
+
+ Preconditions.checkNotNull(currentCidrGroupsMappingLoader);
+ Set<String> cidrGroups = currentCidrGroupsMappingLoader.lookupCidrGroupsCacheForIp(ipAddr);
+
+ cidrAuthorizerMetrics.lookupCidrGroupsForIpLatency.update(MonotonicClock.Global.approxTime.now() - startTimeNanos,
+ TimeUnit.NANOSECONDS);
+
+ return cidrGroups;
+ }
+
+ /**
+ * Lookup best matching CIDR group for the given IP
+ * @param ipAddr IP address
+ * @return returns set of CIDR group(s)
+ */
+ public Set<String> lookupCidrGroupsForIp(InetAddress ipAddr)
+ {
+ Preconditions.checkState(cacheInitialized, "CIDR groups cache not inited");
+ return ipToCidrGroupsCache.get(ipAddr);
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingIntervalTree.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingIntervalTree.java
new file mode 100644
index 0000000000..2dcb652b1e
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingIntervalTree.java
@@ -0,0 +1,504 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.cql3.CIDR;
+
+/**
+ * This class implements CIDR Interval tree and the ability to find longest matching CIDR for the given IP.
+ * CIDRs interval tree is a variant of interval tree. Each node contains a CIDR and a value.
+ * A node has left children array and the right children array.
+ * - The left children's CIDRs are either less than the starting IP of parent or overlaps with the parent node.
+ * - The right children's CIDRs are either greater than the ending IP of the parent or overlaps with the parent node.
+ * Note that nodes that overlap with the parent node are included in both left and right children arrays.
+ *
+ * The tree organizes nodes by placing non-overlapping CIDRs at the same level. In general, CIDRs with the same net mask
+ * do not overlap, hence are placed in the same level. CIDRs with different net mask may overlap, hence placed at
+ * different levels in the tree. In addition to this, there is an optimisation to promote a CIDR to an upper level, if
+ * it is not overlapping with any CIDR in the parent level, that means, in such cases a CIDR with different net mask can
+ * co-locate in the same level with other CIDRs.
+ *
+ * Levels closer to the root contains CIDRs with higher net mask value. Net mask value decreases as levels further down
+ * from the root. i.e, Nearer the level to the root, the narrower the CIDR, meaning matching the longer IP prefix.
+ *
+ * Search for Longest matching CIDR for an IP starts at level 0, if not found a match, search continues to the next
+ * level, until it finds a match or reaches leaf nodes without a match. That means search terminates on the first match
+ * closest to the root, i.e, locates narrowest matching CIDR.
+ *
+ * Example:
+ * Assume below CIDRs
+ * "128.10.120.2/10", ==> IP range 128.0.0.0 - 128.63.255.255, netmask 10
+ * "128.20.120.2/20", ==> IP range 128.20.112.0 - 128.20.127.255, netmask 20
+ * "0.0.0.0/0", ==> IP range 0.0.0.0 - 255.255.255.255, netmask 0
+ * "10.1.1.2/10" ==> IP range 10.0.0.0 - 10.63.255.255, netmask 10
+ *
+ * Resulting interval tree looks like:
+ *
+ * (10.0.0.0 - 10.63.255.255, 10) (128.20.112.0 - 128.20.127.255, 20)
+ * / \ / \
+ * / (128.0.0.0 - 128.63.255.255, 10)
+ * / / \
+ * (0.0.0.0 - 255.255.255.255, 0)
+ *
+ * Note that in this example (10.0.0.0 - 10.63.255.255, 10) doesn't have any overlapping CIDR, hence moved up a level as
+ * an optimization
+ */
+public class CIDRGroupsMappingIntervalTree<V> implements CIDRGroupsMappingTable<V>
+{
+ private final IPIntervalTree<V> tree;
+
+ /**
+ * Build an interval tree for given CIDRs
+ * @param isIPv6 true if this CIDR groups mapping is for IPv6 IPs, false otherwise
+ * @param cidrMappings CIDR to Value mappings
+ */
+ public CIDRGroupsMappingIntervalTree(boolean isIPv6, Map<CIDR, Set<V>> cidrMappings)
+ {
+ for (CIDR cidr : cidrMappings.keySet())
+ {
+ if (isIPv6 != cidr.isIPv6())
+ throw new IllegalArgumentException("Invalid CIDR format, expecting " + getIPTypeString(isIPv6) +
+ ", received " + getIPTypeString(cidr.isIPv6()));
+ }
+
+ this.tree = IPIntervalTree.build(new ArrayList<>(cidrMappings
+ .entrySet()
+ .stream()
+ .collect(Collectors.groupingBy(p -> p.getKey().getNetMask(),
+ TreeMap::new,
+ Collectors.toList()))
+ .descendingMap()
+ .values()));
+ }
+
+ /**
+ * Get the longest matching CIDR (i.e, the narrowest match) for given IP
+ * @param ip IP to lookup CIDR group
+ * @return CIDR group name(s)
+ */
+ public Set<V> lookupLongestMatchForIP(InetAddress ip)
+ {
+ // Valid when empty CIDR groups mappings received, i.e, cidr_groups table is empty
+ if (tree == null)
+ return Collections.emptySet();
+
+ return tree.query(ip);
+ }
+
+ /**
+ * This class represents a node of an IP interval tree.
+ * A node contains a CIDR, value associated with it, left and right children
+ */
+ static class IPIntervalNode<V>
+ {
+ private final CIDR cidr;
+ private final Set<V> values = new HashSet<>();
+ private IPIntervalNode<V>[] left;
+ private IPIntervalNode<V>[] right;
+
+ public IPIntervalNode(CIDR cidr, Set<V> values, IPIntervalNode<V>[] children)
+ {
+ this.cidr = cidr;
+ if (values != null)
+ this.values.addAll(values);
+ updateChildren(children, true, true);
+ }
+
+ @VisibleForTesting
+ CIDR cidr()
+ {
+ return cidr;
+ }
+
+ @VisibleForTesting
+ IPIntervalNode<V>[] left()
+ {
+ return left;
+ }
+
+ @VisibleForTesting
+ IPIntervalNode<V>[] right()
+ {
+ return right;
+ }
+
+ private void updateLeft(IPIntervalNode<V>[] newValue, boolean shouldUpdate)
+ {
+ if (shouldUpdate)
+ this.left = newValue;
+ }
+
+ private void updateRight(IPIntervalNode<V>[] newValue, boolean shouldUpdate)
+ {
+ if (shouldUpdate)
+ this.right = newValue;
+ }
+
+ /**
+ * Split the children array according to the IP range of this node, and link the nodes to either
+ * the left or right children.
+ * @param children array of nodes which are children for this node
+ * @param updateLeft true to update left children, false otherwise
+ * @param updateRight true to update right children, false otherwise
+ */
+ private void updateChildren(IPIntervalNode<V>[] children, boolean updateLeft, boolean updateRight)
+ {
+ // this is leaf node
+ if (children == null)
+ {
+ updateLeft(null, updateLeft);
+ updateRight(null, updateRight);
+ return;
+ }
+
+ // Find the node in the children that is the closest to this node.
+ int index = binarySearchNodesIndex(children, this.cidr.getStartIpAddress());
+ IPIntervalNode<V> closest = children[index];
+
+ // Scenario - all children nodes are greater than this node
+ if (index == 0 && CIDR.compareIPs(this.cidr.getEndIpAddress(), closest.cidr.getStartIpAddress()) < 0)
+ {
+ updateLeft(null, updateLeft);
+ updateRight(children, updateRight);
+ }
+ // Scenario - all children nodes are lower than this node
+ else if (index == children.length - 1 &&
+ CIDR.compareIPs(this.cidr.getStartIpAddress(), closest.cidr.getEndIpAddress()) > 0)
+ {
+ updateLeft(children, updateLeft);
+ updateRight(null, updateRight);
+ }
+ else // Scenario - part of the children nodes are lower, and the other are greater
+ {
+ // When this node does not overlap with the closest, split the array and
+ // link left and right children correspondingly.
+ if (CIDR.compareIPs(this.cidr.getStartIpAddress(), closest.cidr.getEndIpAddress()) > 0)
+ {
+ // including the closest (node at index) in left
+ updateLeft(Arrays.copyOfRange(children, 0, index + 1), updateLeft);
+ // put the rest in right
+ updateRight(Arrays.copyOfRange(children, index + 1, children.length), updateRight);
+ }
+ else // When the node overlaps, include the closest node in both its left and right children nodes.
+ {
+ // The parent node overlaps with at most 1 interval in the children, because of nature of the CIDR.
+ // Increasing the bit mask by 1, divides the range into halfs.
+ // Note that the node@index is included in both left and right
+ // it is because the current interval partially overlaps with the closest interval
+ // The overlapping interval should always be searched if we cannot find an exact match with current interval.
+ updateLeft(Arrays.copyOfRange(children, 0, index + 1), updateLeft);
+ updateRight(Arrays.copyOfRange(children, index, children.length), updateRight);
+ }
+ }
+ }
+
+ private void updateLeftIfNull(IPIntervalNode<V>[] children)
+ {
+ if (left != null)
+ return;
+
+ updateChildren(children, true, false);
+ }
+
+ private void updateRightIfNull(IPIntervalNode<V>[] children)
+ {
+ if (right != null)
+ return;
+
+ updateChildren(children, false, true);
+ }
+
+ /**
+ * Binary search given array of nodes and return index of the closest matching node.
+ * It looks up for the interval that matches exactly or is left (lower) to the ip.
+ * @param nodes array of nodes
+ * @param ip IP address to search
+ * @param <V> data type of the value
+ * @return index of the closest node
+ */
+ static <V> int binarySearchNodesIndex(IPIntervalNode<V>[] nodes, InetAddress ip)
+ {
+ int start = 0; // inclusive
+ int end = nodes.length; // exclusive
+
+ while (true)
+ {
+ if (start >= end)
+ {
+ // return the closest
+ return Math.max((end - 1), 0);
+ }
+
+ int mid = start + (end - start) / 2;
+ IPIntervalNode<V> midNode = nodes[mid];
+ int cmp = CIDR.compareIPs(ip, midNode.cidr.getStartIpAddress());
+
+ if (cmp == 0) // found the node
+ {
+ return mid;
+ }
+ else if (cmp < 0) // Given IP is less than middle node's starting IP, search left side sub array
+ {
+ end = mid;
+ }
+ else // Given IP is >= middle node's starting IP, so compare ending IP
+ {
+ int compEnd = CIDR.compareIPs(ip, midNode.cidr.getEndIpAddress());
+ // Given IP is >= middle node's starting IP and <= than the ending IP, found the match
+ if (compEnd <= 0)
+ {
+ return mid;
+ }
+ else // IP > middle node's end IP >= given IP, search right side sub array
+ {
+ start = mid + 1;
+ }
+ }
+ }
+ }
+
+ /**
+ * Binary search given array of nodes to find the closest IP interval to the input IP
+ * @param nodes array of nodes
+ * @param ip IP address to search
+ * @param <V> data type of the value
+ * @return the closest node to the input IP
+ */
+ static <V> IPIntervalNode<V> binarySearchNodes(IPIntervalNode<V>[] nodes, InetAddress ip)
+ {
+ int index = binarySearchNodesIndex(nodes, ip);
+ return nodes[index];
+ }
+
+ /**
+ * Search the tree for a CIDR matching given IP. Uses DFS and stops on first match i.e, finds the closest match
+ * to the root, which is the narrowest matching CIDR
+ * @param root subtree with this node as root
+ * @param ip IP address to search
+ * @param <V> data type of the value
+ * @return value(s) associated with the CIDR matching the given IP, Returns null if no match found
+ */
+ static <V> Set<V> query(IPIntervalNode<V> root, InetAddress ip)
+ {
+ IPIntervalNode<V> current = root;
+ while (true) // while loop transformed from tail recursion
+ {
+ boolean largerThanStart = CIDR.compareIPs(ip, current.cidr.getStartIpAddress()) >= 0;
+ boolean lessThanEnd = CIDR.compareIPs(ip, current.cidr.getEndIpAddress()) <= 0;
+ if (largerThanStart && lessThanEnd)
+ {
+ return current.values;
+ }
+ else
+ {
+ IPIntervalNode<V>[] candidates = largerThanStart ? current.right : current.left;
+ // the tree is exhausted, and we are unable to find a match
+ if (candidates == null)
+ {
+ return null;
+ }
+ current = binarySearchNodes(candidates, ip);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * This class represents an interval tree for CIDRs
+ * @param <V> data type of the value
+ */
+ static class IPIntervalTree<V>
+ {
+ // References to first level nodes of the tree
+ private final IPIntervalNode<V>[] level0;
+ // depth of the tree
+ private final int depth;
+
+ private IPIntervalTree(IPIntervalNode<V>[] nodes, int depth)
+ {
+ this.level0 = nodes;
+ this.depth = depth;
+ }
+
+ @VisibleForTesting
+ int getDepth()
+ {
+ return depth;
+ }
+
+ /**
+ * Optimize levels by moving non-overlapping CIDRs from lower level to the upper level. Levels are updated in-place
+ * This optimization moves CIDRs closer to the root, hence improves the search to find IP nearer to the root,
+ * i.e, avoiding going depth during the search
+ * @param upperLevel level for CIDRs with higher netmask value
+ * @param lowerLevel level for CIDRs with lower netmask value
+ * @param <V> data type of value
+ */
+ private static <V> void optimizeLevels(List<Map.Entry<CIDR, V>> upperLevel, List<Map.Entry<CIDR, V>> lowerLevel)
+ {
+ List<Map.Entry<CIDR, V>> newUpper = new ArrayList<>(upperLevel.size() + lowerLevel.size());
+ newUpper.addAll(upperLevel);
+
+ List<Map.Entry<CIDR, V>> newLower = new ArrayList<>(lowerLevel.size());
+
+ for (int i = 0; i < lowerLevel.size(); i++)
+ {
+ boolean noOverlap = true;
+ for (int j = 0; j < upperLevel.size(); j++)
+ {
+ if (CIDR.overlaps(lowerLevel.get(i).getKey(), upperLevel.get(j).getKey()))
+ {
+ // overlapping node remains in lower level
+ newLower.add(lowerLevel.get(i));
+ noOverlap = false;
+ break;
+ }
+ }
+
+ // the node from lower level does not overlap with any node in upper level, so move it up
+ if (noOverlap)
+ {
+ newUpper.add(lowerLevel.get(i));
+ }
+ }
+
+ upperLevel.clear();
+ lowerLevel.clear();
+ upperLevel.addAll(newUpper);
+ lowerLevel.addAll(newLower);
+ }
+
+ /**
+ * Optimize by lifting lower level non-overlapping CIDRs to the upper levels.
+ * The method modifies cidrsGroupedByNetMasks in-place.
+ * @param cidrsGroupedByNetMasks CIDRs grouped by netmask, levels in the order of higher netmask to lower netmask value
+ * @param <V> data type of the value
+ */
+ private static <V> void optimizeAllLevels(List<List<Map.Entry<CIDR, Set<V>>>> cidrsGroupedByNetMasks)
+ {
+ for (int i = 0; i < cidrsGroupedByNetMasks.size(); i++)
+ {
+ List<Map.Entry<CIDR, Set<V>>> current = cidrsGroupedByNetMasks.get(0);
+ for (int j = i + 1; j < cidrsGroupedByNetMasks.size(); j++)
+ {
+ List<Map.Entry<CIDR, Set<V>>> lower = cidrsGroupedByNetMasks.get(j);
+ optimizeLevels(current, lower);
+ }
+ }
+ }
+
+ /**
+ * Link the nodes between levels. If a node in the lower level has no left or right children,
+ * the nodes from higher levels are lifted and linked as the left or right children.
+ * @param cidrMappings list of CIDR to value mappings
+ * @param result 2 dimension array that stores all (linked) nodes in the tree. result[0] is the first level in the tree.
+ * @param startIndex starting level in the result array, to link it to its children nodes
+ * @param <V> data type of the value
+ */
+ @SuppressWarnings("unchecked")
+ private static <V> void linkNodes(List<List<Map.Entry<CIDR, Set<V>>>> cidrMappings,
+ IPIntervalNode<V>[][] result,
+ int startIndex)
+ {
+ List<Map.Entry<CIDR, Set<V>>> cidrsAtLevel = cidrMappings.get(startIndex);
+ int next = startIndex + 1;
+ IPIntervalNode<V>[] lowerLevel = next == result.length ? null : result[next];
+ result[startIndex] = cidrsAtLevel
+ .stream()
+ .map(pair -> {
+ CIDR cidr = pair.getKey();
+ Set<V> value = pair.getValue();
+ IPIntervalNode<V> node = new IPIntervalNode<>(cidr, value, lowerLevel);
+ // Cannot link its left or right children to the next level nodes.
+ // Try the nodes in the even lower levels.
+ if (next + 1 < result.length && (node.left == null || node.right == null))
+ {
+ for (int i = next + 1; i < result.length; i++)
+ {
+ node.updateLeftIfNull(result[i]);
+ node.updateRightIfNull(result[i]);
+
+ // the node has both left and right children, it is good and no further lifting is needed.
+ if (node.left != null && node.right != null)
+ {
+ break;
+ }
+ }
+ }
+ return node;
+ })
+ // sort by ipStart. Remember the interval do not overlap
+ .sorted(Comparator.comparing(n -> n.cidr.getStartIpAddress(), CIDR::compareIPs))
+ .toArray(IPIntervalNode[]::new);
+ }
+
+ /**
+ * Build an interval tree for given CIDRs
+ * @param cidrsGroupedByNetMasks CIDRs grouped by netmask, levels in the order of higher netmask to lower netmask value
+ * @param <V> data type of the value
+ * @return returns reference to the interval tree, returns null if input is empty
+ */
+ @SuppressWarnings("unchecked")
+ public static <V> IPIntervalTree<V> build(List<List<Map.Entry<CIDR, Set<V>>>> cidrsGroupedByNetMasks)
+ {
+ if (cidrsGroupedByNetMasks.isEmpty())
+ return null;
+
+ optimizeAllLevels(cidrsGroupedByNetMasks);
+
+ // After optimization, trim empty levels if exists
+ cidrsGroupedByNetMasks.removeIf(List::isEmpty);
+
+ // Create an array for each level
+ IPIntervalNode<V>[][] result = new IPIntervalNode[cidrsGroupedByNetMasks.size()][];
+ // build the tree bottom up, i.e, add CIDRs in the order of smaller netmask value to higher netmask value
+ for (int i = cidrsGroupedByNetMasks.size() - 1; i >= 0; i--)
+ {
+ linkNodes(cidrsGroupedByNetMasks, result, i);
+ }
+ return new IPIntervalTree<>(result[0], cidrsGroupedByNetMasks.size());
+ }
+
+ /**
+ * Search interval tree for the longest matching CIDR for the given IP
+ * @param ip IP address to search
+ * @return Value(s) associated with matching CIDR
+ */
+ public Set<V> query(InetAddress ip)
+ {
+ IPIntervalNode<V> closest = IPIntervalNode.binarySearchNodes(level0, ip);
+ return IPIntervalNode.query(closest, ip);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingLoader.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingLoader.java
new file mode 100644
index 0000000000..42ba904b28
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingLoader.java
@@ -0,0 +1,95 @@
+/*
+ * 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.auth;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.utils.Pair;
+
+/**
+ * Reads the table {@link AuthKeyspace#CIDR_GROUPS} and populates CIDR groups mapping data structures.
+ * Maintains separate mappings for IPv4 and IPv6 CIDRs
+ */
+public class CIDRGroupsMappingLoader
+{
+ private static final Logger logger = LoggerFactory.getLogger(CIDRGroupsMappingLoader.class);
+
+ CIDRGroupsMappingTable.Builder<String> ipv4CidrGroupsTableBuilder = CIDRGroupsMappingTable.builder(false);
+ CIDRGroupsMappingTable.Builder<String> ipv6CidrGroupsTableBuilder = CIDRGroupsMappingTable.builder(true);
+
+ // Maintain separate mappings for IPv4 and IPv6 CIDRs
+ private CIDRGroupsMappingTable<String> ipv4CidrGroupsTable = null;
+ private CIDRGroupsMappingTable<String> ipv6CidrGroupsTable = null;
+
+ protected CIDRGroupsMappingManager cidrGroupsMappingManager;
+
+ public CIDRGroupsMappingLoader(CIDRGroupsMappingManager cidrGroupsMappingManager)
+ {
+ this.cidrGroupsMappingManager = cidrGroupsMappingManager;
+
+ populateCidrGroupsMapping();
+ }
+
+ private void populateCidrGroupsMapping()
+ {
+ UntypedResultSet rows = cidrGroupsMappingManager.getCidrGroupsTableEntries();
+ for (UntypedResultSet.Row row : rows)
+ {
+ String cidrGroupName = row.getString(AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME);
+ Set<Pair<InetAddress, Short>> cidrs = cidrGroupsMappingManager.retrieveCidrsFromRow(row);
+
+ for (Pair<InetAddress, Short> cidr : cidrs)
+ {
+ try
+ {
+ CIDR validCidr = new CIDR(cidr.left(), cidr.right);
+ if (validCidr.isIPv6())
+ ipv6CidrGroupsTableBuilder.add(validCidr, cidrGroupName);
+ else
+ ipv4CidrGroupsTableBuilder.add(validCidr, cidrGroupName);
+ }
+ catch (RuntimeException e)
+ {
+ // We should not have this scenario after introducing CIDR cql datatype which ensures
+ // invalid CIDRs can't be inserted into the table using insert/update cql commands
+ logger.error("Invalid CIDR {} found in the table, ignoring this entry", cidr);
+ }
+ }
+ }
+
+ ipv4CidrGroupsTable = ipv4CidrGroupsTableBuilder.build();
+ ipv6CidrGroupsTable = ipv6CidrGroupsTableBuilder.build();
+ }
+
+ public Set<String> lookupCidrGroupsCacheForIp(InetAddress ipAddr)
+ {
+ // Based on the incoming IP format, lookup in appropriate CIDR groups mapping
+ if (ipAddr instanceof Inet6Address)
+ return ipv6CidrGroupsTable.lookupLongestMatchForIP(ipAddr);
+ else
+ return ipv4CidrGroupsTable.lookupLongestMatchForIP(ipAddr);
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManager.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManager.java
new file mode 100644
index 0000000000..6659bdcc15
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManager.java
@@ -0,0 +1,251 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.marshal.ByteBufferAccessor;
+import org.apache.cassandra.db.marshal.InetAddressType;
+import org.apache.cassandra.db.marshal.ShortType;
+import org.apache.cassandra.db.marshal.TupleType;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.transport.messages.ResultMessage;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.MBeanWrapper;
+import org.apache.cassandra.utils.Pair;
+
+import static org.apache.cassandra.service.QueryState.forInternalCalls;
+import static org.apache.cassandra.utils.Clock.Global.nanoTime;
+
+
+public class CIDRGroupsMappingManager implements CIDRGroupsMappingManagerMBean
+{
+ public static final String MBEAN_NAME = "org.apache.cassandra.db:type=CIDRGroupsMappingManager";
+
+ private SelectStatement getCidrGroupsStatement;
+ private SelectStatement getCidrsForCidrGroupStatement;
+
+ private static final int PAGE_SIZE = 128;
+
+ public void setup()
+ {
+ if (!MBeanWrapper.instance.isRegistered(MBEAN_NAME))
+ MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
+
+ String getCidrGroupsQuery = String.format("SELECT %s FROM %s.%s",
+ AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME,
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_GROUPS);
+ getCidrGroupsStatement = (SelectStatement) QueryProcessor.getStatement(getCidrGroupsQuery,
+ ClientState.forInternalCalls());
+
+ String getCidrsForCidrGroupQuery = String.format("SELECT %s FROM %s.%s where %s = ?",
+ AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME,
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_GROUPS,
+ AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME);
+ getCidrsForCidrGroupStatement = (SelectStatement) QueryProcessor.getStatement(getCidrsForCidrGroupQuery,
+ ClientState.forInternalCalls());
+ }
+
+ @VisibleForTesting
+ ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
+ {
+ return statement.execute(forInternalCalls(), options, nanoTime());
+ }
+
+ UntypedResultSet process(String query, ConsistencyLevel cl) throws RequestExecutionException
+ {
+ return QueryProcessor.process(query, cl);
+ }
+
+ protected static String getCidrTuplesSetString(List<CIDR> cidrs)
+ {
+ String inner = cidrs.stream().map(CIDR::asCqlTupleString)
+ .collect(Collectors.joining(", "));
+ return '{' + inner + '}';
+ }
+
+ public UntypedResultSet getCidrGroupsTableEntries()
+ {
+ String getAllRowsQuery = String.format("SELECT * FROM %s.%s",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_GROUPS);
+
+ return QueryProcessor.executeInternalWithPaging(getAllRowsQuery, PAGE_SIZE);
+ }
+
+ public Set<String> getAvailableCidrGroups()
+ {
+ Set<String> availableCidrGroups = new HashSet<>();
+
+ QueryOptions options = QueryOptions.forInternalCalls(CassandraAuthorizer.authReadConsistencyLevel(),
+ Collections.emptyList());
+
+ ResultMessage.Rows rows = select(getCidrGroupsStatement, options);
+ UntypedResultSet result = UntypedResultSet.create(rows.result);
+
+ for (UntypedResultSet.Row row : result)
+ {
+ if (!row.has(AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME))
+ throw new IllegalStateException("Invalid row " + row + " in table: " +
+ SchemaConstants.AUTH_KEYSPACE_NAME + '.' + AuthKeyspace.CIDR_GROUPS);
+
+ availableCidrGroups.add(row.getString(AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME));
+ }
+
+ return availableCidrGroups;
+ }
+
+ public Set<Pair<InetAddress, Short>> retrieveCidrsFromRow(UntypedResultSet.Row row)
+ {
+ if (!row.has(AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME))
+ throw new RuntimeException("Invalid row, doesn't have column " +
+ AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME);
+
+ Set<Pair<InetAddress, Short>> cidrs = new HashSet<>();
+ TupleType tupleType = new TupleType(Arrays.asList(InetAddressType.instance, ShortType.instance));
+
+ Set<ByteBuffer> cidrAsTuples = row.getFrozenSet(AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME, tupleType);
+ for (ByteBuffer cidrAsTuple : cidrAsTuples)
+ {
+ ByteBuffer[] splits = tupleType.split(ByteBufferAccessor.instance, cidrAsTuple);
+ InetAddress ip = InetAddressType.instance.compose(splits[0]);
+ short netMask = ShortType.instance.compose(splits[1]);
+ cidrs.add(Pair.create(ip, netMask));
+ }
+
+ return cidrs;
+ }
+
+ public Set<String> getCidrsOfCidrGroupAsStrings(String cidrGroupName)
+ {
+ QueryOptions options = QueryOptions.forInternalCalls(CassandraAuthorizer.authReadConsistencyLevel(),
+ Lists.newArrayList(ByteBufferUtil.bytes(cidrGroupName)));
+ ResultMessage.Rows rows = select(getCidrsForCidrGroupStatement, options);
+ UntypedResultSet result = UntypedResultSet.create(rows.result);
+
+ if (result.isEmpty())
+ return Collections.emptySet();
+
+ Set<Pair<InetAddress, Short>> allCidrs = retrieveCidrsFromRow(result.one());
+ Set<String> cidrStrs = new HashSet<>();
+ for (Pair<InetAddress, Short> cidr : allCidrs)
+ cidrStrs.add(cidr.left().getHostAddress() + '/' + cidr.right());
+
+ return cidrStrs;
+ }
+
+ public void updateCidrGroup(String cidrGroupName, List<String> cidrs)
+ {
+ List<CIDR> validCidrs = new ArrayList<>();
+ for (String cidr : cidrs)
+ {
+ validCidrs.add(CIDR.getInstance(cidr));
+ }
+
+ String query = String.format("UPDATE %s.%s SET %s = %s WHERE %s = '%s'",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_GROUPS,
+ AuthKeyspace.CIDR_GROUPS_TBL_CIDRS_COL_NAME,
+ getCidrTuplesSetString(validCidrs),
+ AuthKeyspace.CIDR_GROUPS_TBL_CIDR_GROUP_COL_NAME,
+ cidrGroupName);
+
+ process(query, CassandraAuthorizer.authWriteConsistencyLevel());
+ }
+
+ @VisibleForTesting
+ void dropCidrGroupIfExists(String cidrGroupName)
+ {
+ String query = String.format("DELETE FROM %s.%s WHERE cidr_group = '%s'",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_GROUPS,
+ cidrGroupName);
+
+ process(query, CassandraAuthorizer.authWriteConsistencyLevel());
+ }
+
+ public void dropCidrGroup(String cidrGroupName)
+ {
+ Set<String> cidrs = getCidrsOfCidrGroupAsStrings(cidrGroupName);
+ if (cidrs.isEmpty())
+ throw new RuntimeException("CIDR group '" + cidrGroupName + "' doesn't exists");
+
+ dropCidrGroupIfExists(cidrGroupName);
+ }
+
+ public void recreateCidrGroupsMapping(Map<String, List<String>> cidrGroupsMapping)
+ {
+ Set<String> existingMappings = getAvailableCidrGroups();
+
+ // Overwrites mappings of existing cidr groups and inserts new cidr groups
+ for (Map.Entry<String, List<String>> cidrGroupMapping : cidrGroupsMapping.entrySet())
+ {
+ String cidrGroupName = cidrGroupMapping.getKey();
+ updateCidrGroup(cidrGroupName, cidrGroupMapping.getValue());
+ existingMappings.remove(cidrGroupName);
+ }
+
+ // Delete old CIDR groups which do not exist in new mappings
+ for (String cidrGroupName : existingMappings)
+ {
+ dropCidrGroupIfExists(cidrGroupName);
+ }
+ }
+
+ public Set<String> getCidrGroupsOfIP(String ipStr)
+ {
+ try
+ {
+ return DatabaseDescriptor.getCIDRAuthorizer().lookupCidrGroupsForIp(InetAddress.getByName(ipStr));
+ }
+ catch (UnknownHostException e)
+ {
+ throw new IllegalArgumentException("Invalid IP " + ipStr, e);
+ }
+ }
+
+ public void loadCidrGroupsCache()
+ {
+ DatabaseDescriptor.getCIDRAuthorizer().loadCidrGroupsCache();
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManagerMBean.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManagerMBean.java
new file mode 100644
index 0000000000..3e8223936e
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingManagerMBean.java
@@ -0,0 +1,79 @@
+/*
+ * 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.auth;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class provides utility functions to list/update/drop CIDR groups mappings.
+ * nodetool commands invoke these functions, hence need to register mbean even when
+ * CIDR authorization is disabled
+ */
+public interface CIDRGroupsMappingManagerMBean
+{
+ /**
+ * Retrieves available CIDR groups in the table {@link AuthKeyspace#CIDR_GROUPS}
+ * @return returns CIDR groups set
+ */
+ public Set<String> getAvailableCidrGroups();
+
+ /**
+ * Retrieves all CIDRs associated with a given CIDR group in the table {@link AuthKeyspace#CIDR_GROUPS},
+ * including invalid CIDRs (if exists)
+ * @param cidrGroupName CIDR group to retrieve associated CIDRs
+ * @return returns set of CIDRs as strings
+ */
+ public Set<String> getCidrsOfCidrGroupAsStrings(String cidrGroupName);
+
+ /**
+ * Insert/Update CIDR group to CIDRs mapping in the table {@link AuthKeyspace#CIDR_GROUPS}
+ * @param cidrGroupName CIDR group name
+ * @param cidrs List of CIDRs as strings
+ */
+ public void updateCidrGroup(String cidrGroupName, List<String> cidrs);
+
+ /**
+ * Delete a CIDR group and associated mapping from the table {@link AuthKeyspace#CIDR_GROUPS}
+ * throws exception if input cidr group name doesn't exist in the table before deletion
+ * @param cidrGroupName CIDR group name
+ */
+ public void dropCidrGroup(String cidrGroupName);
+
+ /**
+ * Update {@link AuthKeyspace#CIDR_GROUPS} table with completly new mappings. Insert nonexisting CIDR groups,
+ * update existing CIDR groups with given set of CIDRs and delete nolonger valid CIDR groups.
+ * @param cidrGroupsMapping CIDR group mappings
+ */
+ public void recreateCidrGroupsMapping(Map<String, List<String>> cidrGroupsMapping);
+
+ /**
+ * Get CIDR groups of a given IP, when CIDR authorizer is enabled.
+ * Throws exception if CIDR authorizer is disabled
+ * @param ipStr IP as a string
+ * @return Set of strings
+ */
+ public Set<String> getCidrGroupsOfIP(String ipStr);
+
+ /**
+ * Trigger reload of CIDR groups cache
+ */
+ public void loadCidrGroupsCache();
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRGroupsMappingTable.java b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingTable.java
new file mode 100644
index 0000000000..8ebd21ff7a
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRGroupsMappingTable.java
@@ -0,0 +1,80 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cassandra.cql3.CIDR;
+
+/**
+ * This interface defines functionality to be provided by algorithm(s) implementing CIDR to CIDR groups mappings,
+ * to facilitate the efficient way to find the longest matching CIDR for a given IP
+ */
+public interface CIDRGroupsMappingTable<V>
+{
+ default String getIPTypeString(boolean isIPv6)
+ {
+ return isIPv6 ? "IPv6" : "IPv4";
+ }
+
+ static <V> Builder<V> builder(boolean isIPv6)
+ {
+ return new Builder<>(isIPv6);
+ }
+
+ /**
+ * Finds the longest matching CIDR for the given IP
+ * @param ip IP to lookup CIDR group
+ * @return returns CIDR group(s) associated with the longest matching CIDR for the given IP, returns null if no match found
+ */
+ public Set<V> lookupLongestMatchForIP(InetAddress ip);
+
+
+ /**
+ * Builder to add CIDR to CIDR groups mappings and construct a mapping table with them
+ */
+ class Builder<V>
+ {
+ private final Map<CIDR, Set<V>> cidrMappings = new HashMap<>();
+ private final boolean isIPv6;
+
+ private Builder(boolean isIPv6)
+ {
+ this.isIPv6 = isIPv6;
+ }
+
+ public void add(CIDR cidr, V cidrGroup)
+ {
+ cidrMappings.computeIfAbsent(cidr, k -> new HashSet<>()).add(cidrGroup);
+ }
+
+ /**
+ * Provides an instance of algorithm implementing CIDR to CIDR groups mappings
+ * @return returns instance of CIDRGroupsMappingTable
+ */
+ public CIDRGroupsMappingTable<V> build()
+ {
+ return new CIDRGroupsMappingIntervalTree<>(isIPv6, cidrMappings);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRPermissions.java b/src/java/org/apache/cassandra/auth/CIDRPermissions.java
new file mode 100644
index 0000000000..c4870f2a90
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRPermissions.java
@@ -0,0 +1,257 @@
+/*
+ * 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.auth;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.StringJoiner;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+
+/**
+ * Contains CIDR permissions of a role
+ */
+public abstract class CIDRPermissions
+{
+ /**
+ * Determines whether this permissions object allows access from given cidr group(s)
+ * @param cidrGroup set of CIDR groups
+ * @return returns true if role has access, otherwise false
+ */
+ public abstract boolean canAccessFrom(Set<String> cidrGroup);
+
+ /**
+ * Determines whether this permissions object restricts access from some CIDR groups
+ * or allows access from any CIDR group
+ * @return true if restricts access, otherwise false
+ */
+ public abstract boolean restrictsAccess();
+
+ /**
+ * Returns CIDR permissions allowed by this object
+ * @return returns set of CIDR groups
+ */
+ public abstract Set<String> allowedCIDRGroups();
+
+ /**
+ * Validates does this object contains valid CIDR groups
+ */
+ public abstract void validate();
+
+ // This class represents a subset of CIDR permissions, i.e, not ALL and not NONE.
+ private static class SubsetPermissions extends CIDRPermissions
+ {
+ private final Set<String> subset;
+
+ public SubsetPermissions(Set<String> subset)
+ {
+ Preconditions.checkNotNull(subset);
+ this.subset = subset;
+ }
+
+ public boolean canAccessFrom(Set<String> cidrGroups)
+ {
+ return subset.stream().anyMatch(cidrGroups::contains);
+ }
+
+ public boolean restrictsAccess()
+ {
+ return true;
+ }
+
+ public Set<String> allowedCIDRGroups()
+ {
+ return ImmutableSet.copyOf(subset);
+ }
+
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ SubsetPermissions that = (SubsetPermissions) o;
+
+ return subset.equals(that.subset);
+ }
+
+ public int hashCode()
+ {
+ return subset.hashCode();
+ }
+
+ public String toString()
+ {
+ StringJoiner joiner = new StringJoiner(", ");
+ subset.forEach(joiner::add);
+ return joiner.toString();
+ }
+
+ public void validate()
+ {
+ Set<String> availableCidrGroups = DatabaseDescriptor.getCIDRAuthorizer()
+ .getCidrGroupsMappingManager()
+ .getAvailableCidrGroups();
+ Set<String> unknownCidrGroups = Sets.difference(subset, availableCidrGroups);
+ if (!unknownCidrGroups.isEmpty())
+ {
+ throw new InvalidRequestException("Invalid CIDR group(s): " + subset + ". Available CIDR Groups are: "
+ + availableCidrGroups);
+ }
+ }
+ }
+
+ private static final CIDRPermissions ALL = new CIDRPermissions()
+ {
+ public boolean canAccessFrom(Set<String> cidrGroup)
+ {
+ return true;
+ }
+
+ public boolean restrictsAccess()
+ {
+ return false;
+ }
+
+ public Set<String> allowedCIDRGroups()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public String toString()
+ {
+ return "ALL";
+ }
+
+ public void validate()
+ {
+
+ }
+ };
+
+ private static final CIDRPermissions NONE = new CIDRPermissions()
+ {
+ public boolean canAccessFrom(Set<String> cidrGroup)
+ {
+ return false;
+ }
+
+ public boolean restrictsAccess()
+ {
+ return true;
+ }
+
+ public Set<String> allowedCIDRGroups()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public String toString()
+ {
+ return "n/a";
+ }
+
+ public void validate()
+ {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ /**
+ * Generates CIDR permissions object which allows from all CIDR groups
+ * @return returns CIDRPermissions object
+ */
+ public static CIDRPermissions all()
+ {
+ return ALL;
+ }
+
+ /**
+ * Generates CIDR permissions object which doesn't allow from any CIDR group
+ * @return returns CIDRPermissions object
+ */
+ public static CIDRPermissions none()
+ {
+ return NONE;
+ }
+
+ /**
+ * Generates CIDR permissions object with given CIDR groups
+ * @param cidrGroups set of CIDR groups
+ * @return returns CIDRPermissions object
+ */
+ public static CIDRPermissions subset(Set<String> cidrGroups)
+ {
+ return new SubsetPermissions(cidrGroups);
+ }
+
+ /**
+ * Builder to generate CIDR Permissions
+ */
+ public static class Builder
+ {
+ private final Set<String> cidrGroups = new HashSet<>();
+ private boolean isAll = false;
+ private boolean modified = false;
+
+ public void add(String cidrGroup)
+ {
+ Preconditions.checkArgument(!isAll, "All has been set");
+ cidrGroups.add(cidrGroup);
+ modified = true;
+ }
+
+ public void all()
+ {
+ Preconditions.checkArgument(cidrGroups.isEmpty(), "CIDR Groups have already been set");
+ isAll = true;
+ modified = true;
+ }
+
+ public boolean isModified()
+ {
+ return modified;
+ }
+
+ public CIDRPermissions build()
+ {
+ if (cidrGroups.isEmpty())
+ {
+ return CIDRPermissions.all();
+ }
+ else
+ {
+ return subset(cidrGroups);
+ }
+ }
+ }
+
+ /**
+ * Builder object to generate CIDR permissions
+ * @return returns Builder object
+ */
+ public static Builder builder()
+ {
+ return new Builder();
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRPermissionsCache.java b/src/java/org/apache/cassandra/auth/CIDRPermissionsCache.java
new file mode 100644
index 0000000000..38c3a5d13f
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRPermissionsCache.java
@@ -0,0 +1,69 @@
+/*
+ * 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.auth;
+
+import java.util.Map;
+import java.util.function.BooleanSupplier;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+public class CIDRPermissionsCache extends AuthCache<RoleResource, CIDRPermissions>
+{
+ public static final String CACHE_NAME = "CidrPermissionsCache";
+
+ public CIDRPermissionsCache(Function<RoleResource, CIDRPermissions> loadFunction,
+ Supplier<Map<RoleResource, CIDRPermissions>> bulkLoadFunction,
+ BooleanSupplier cacheEnabledDelegate)
+ {
+ super(CACHE_NAME,
+ DatabaseDescriptor::setRolesValidity,
+ DatabaseDescriptor::getRolesValidity,
+ DatabaseDescriptor::setRolesUpdateInterval,
+ DatabaseDescriptor::getRolesUpdateInterval,
+ DatabaseDescriptor::setRolesCacheMaxEntries,
+ DatabaseDescriptor::getRolesCacheMaxEntries,
+ DatabaseDescriptor::setRolesCacheActiveUpdate,
+ DatabaseDescriptor::getRolesCacheActiveUpdate,
+ loadFunction,
+ bulkLoadFunction,
+ cacheEnabledDelegate);
+ }
+
+ /**
+ * Invalidate a role from CIDR permissions cache
+ * @param roleName role for which to invalidate the cache
+ * @return boolean returns true if given role found in the cache and invalidated, otherwise returns false
+ */
+ public boolean invalidateCidrPermissions(String roleName)
+ {
+ if (cache.getIfPresent(roleName) == null)
+ return false;
+
+ invalidate(RoleResource.role(roleName));
+ return true;
+ }
+
+ @Override
+ protected void unregisterMBean()
+ {
+ super.unregisterMBean();
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/CIDRPermissionsManager.java b/src/java/org/apache/cassandra/auth/CIDRPermissionsManager.java
new file mode 100644
index 0000000000..459d845bee
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CIDRPermissionsManager.java
@@ -0,0 +1,220 @@
+/*
+ * 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.auth;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.reads.range.RangeCommands;
+import org.apache.cassandra.transport.messages.ResultMessage;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.MBeanWrapper;
+
+import static org.apache.cassandra.service.QueryState.forInternalCalls;
+import static org.apache.cassandra.utils.Clock.Global.nanoTime;
+
+/**
+ * Provides functionality to list/update/drop CIDR permissions of a role
+ * Backend to build Role to CIDR permissions cache
+ */
+public class CIDRPermissionsManager implements CIDRPermissionsManagerMBean, AuthCache.BulkLoader<RoleResource, CIDRPermissions>
+{
+ public static final String MBEAN_NAME = "org.apache.cassandra.db:type=CIDRPermissionsManager";
+
+ private static final Logger logger = LoggerFactory.getLogger(CIDRPermissionsManager.class);
+ private SelectStatement getCidrPermissionsOfUserStatement = null;
+
+ public void setup()
+ {
+ if (!MBeanWrapper.instance.isRegistered(MBEAN_NAME))
+ MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
+
+ String getCidrPermissionsOfUserQuery = String.format("SELECT %s FROM %s.%s WHERE %s = ?",
+ AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME,
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_PERMISSIONS,
+ AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME);
+ getCidrPermissionsOfUserStatement = (SelectStatement) QueryProcessor.getStatement(getCidrPermissionsOfUserQuery,
+ ClientState.forInternalCalls());
+ }
+
+ @VisibleForTesting
+ ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
+ {
+ return statement.execute(forInternalCalls(), options, nanoTime());
+ }
+
+ @VisibleForTesting
+ UntypedResultSet process(String query, ConsistencyLevel cl) throws RequestExecutionException
+ {
+ return QueryProcessor.process(query, cl);
+ }
+
+ private Set<String> getAuthorizedCIDRGroups(String name)
+ {
+ QueryOptions options = QueryOptions.forInternalCalls(CassandraAuthorizer.authReadConsistencyLevel(),
+ Lists.newArrayList(ByteBufferUtil.bytes(name)));
+
+ ResultMessage.Rows rows = select(getCidrPermissionsOfUserStatement, options);
+ UntypedResultSet result = UntypedResultSet.create(rows.result);
+ if (!result.isEmpty() && result.one().has(AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME))
+ {
+ return result.one().getFrozenSet(AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME, UTF8Type.instance);
+ }
+
+ return Collections.emptySet();
+ }
+
+ private static String getCidrPermissionsSetString(CIDRPermissions permissions)
+ {
+ String inner = "";
+
+ if (permissions.restrictsAccess())
+ inner = permissions.allowedCIDRGroups().stream().map(s -> '\'' + s + '\'')
+ .collect(Collectors.joining(", "));
+
+ return '{' + inner + '}';
+ }
+
+ /**
+ * Get CIDR permissions of a role
+ * @param role role for which to get CIDR permissions
+ * @return returns CIDR permissions of a role
+ */
+ public CIDRPermissions getCidrPermissionsForRole(RoleResource role)
+ {
+ if (!Roles.canLogin(role))
+ {
+ return CIDRPermissions.none();
+ }
+ if (Roles.hasSuperuserStatus(role) && !DatabaseDescriptor.getCidrChecksForSuperusers())
+ {
+ return CIDRPermissions.all();
+ }
+
+ Set<String> cidrGroups = getAuthorizedCIDRGroups(role.getRoleName());
+ // User don't have CIDR permissions explicitly enabled, i.e, allow from all
+ if (cidrGroups == null || cidrGroups.isEmpty())
+ {
+ // No explicit CIDR groups set for the role, allow from all
+ return CIDRPermissions.all();
+ }
+
+ return CIDRPermissions.subset(cidrGroups);
+ }
+
+ /**
+ * Set CIDR permissions for a given role
+ * @param role role for which to set CIDR permissions
+ * @param cidrPermissions CIR permissions to set for the role
+ */
+ public void setCidrGroupsForRole(RoleResource role, CIDRPermissions cidrPermissions)
+ {
+ String query = String.format("UPDATE %s.%s SET %s = %s WHERE %s = '%s'",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_PERMISSIONS,
+ AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME,
+ getCidrPermissionsSetString(cidrPermissions),
+ AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME,
+ role.getRoleName());
+
+ process(query, CassandraAuthorizer.authWriteConsistencyLevel());
+ }
+
+ /**
+ * Drop CIDR permissions of a role, i.e, delete corresponding row from the table
+ * @param role for which to drop cidr permissions
+ */
+ public void drop(RoleResource role)
+ {
+ String query = String.format("DELETE FROM %s.%s WHERE role = '%s'",
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_PERMISSIONS,
+ role.getRoleName());
+
+ process(query, CassandraAuthorizer.authWriteConsistencyLevel());
+ }
+
+ /**
+ * Function to bulk load role to cidr permissions cache
+ * This gets called when cache warmup is enabled in the config
+ * @return returns nothing
+ */
+ @Override
+ public Supplier<Map<RoleResource, CIDRPermissions>> bulkLoader()
+ {
+ return () -> {
+ if (!RangeCommands.sufficientLiveNodesForSelectStar(AuthKeyspace.metadata().tables.getNullable(AuthKeyspace.CIDR_PERMISSIONS),
+ AuthProperties.instance.getReadConsistencyLevel()))
+ {
+ // Prevent running the query we know will fail so as not to increment unavailable stats for a performance
+ // optimization
+ throw new RuntimeException("insufficient live nodes for " +
+ AuthProperties.instance.getReadConsistencyLevel() +
+ "pre-warm query again system_auth." + AuthKeyspace.CIDR_PERMISSIONS);
+ }
+
+ logger.info("Pre-warming CIDR permissions cache from cidr_permissions table");
+ Map<RoleResource, CIDRPermissions> entries = new HashMap<>();
+ UntypedResultSet rows = process(String.format("SELECT %s, %s FROM %s.%s",
+ AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME,
+ AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME,
+ SchemaConstants.AUTH_KEYSPACE_NAME,
+ AuthKeyspace.CIDR_PERMISSIONS),
+ CassandraAuthorizer.authReadConsistencyLevel());
+
+ for (UntypedResultSet.Row row : rows)
+ {
+ RoleResource role = RoleResource.role(row.getString(AuthKeyspace.CIDR_PERMISSIONS_TBL_ROLE_COL_NAME));
+ CIDRPermissions.Builder builder = new CIDRPermissions.Builder();
+ Set<String> cidrGroups = row.getFrozenSet(AuthKeyspace.CIDR_PERMISSIONS_TBL_CIDR_GROUPS_COL_NAME,
+ UTF8Type.instance);
+ for (String cidrGroup : cidrGroups)
+ builder.add(cidrGroup);
+ entries.put(role, builder.build());
+ }
+
+ return entries;
+ };
+ }
+
+ public boolean invalidateCidrPermissionsCache(String roleName)
+ {
+ return DatabaseDescriptor.getCIDRAuthorizer().invalidateCidrPermissionsCache(roleName);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java b/src/java/org/apache/cassandra/auth/CIDRPermissionsManagerMBean.java
similarity index 50%
copy from test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
copy to src/java/org/apache/cassandra/auth/CIDRPermissionsManagerMBean.java
index 51c61bbb12..c5376300c6 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
+++ b/src/java/org/apache/cassandra/auth/CIDRPermissionsManagerMBean.java
@@ -16,32 +16,17 @@
* limitations under the License.
*/
-package org.apache.cassandra.cql3.statements;
+package org.apache.cassandra.auth;
-import org.junit.Assert;
-import org.junit.Test;
-
-import org.apache.cassandra.auth.DCPermissions;
-import org.apache.cassandra.cql3.CQLStatement;
-import org.apache.cassandra.cql3.QueryProcessor;
-
-public class CreateUserStatementTest
+/**
+ * JMX functions related to CIDR permissions cache and table
+ */
+public interface CIDRPermissionsManagerMBean
{
- private static CreateRoleStatement parse(String query)
- {
- CQLStatement.Raw stmt = QueryProcessor.parseStatement(query);
- Assert.assertTrue(stmt instanceof CreateRoleStatement);
- return (CreateRoleStatement) stmt;
- }
-
- private static DCPermissions dcPerms(String query)
- {
- return parse(query).dcPermissions;
- }
-
- @Test
- public void allDcsImplicit() throws Exception
- {
- Assert.assertFalse(dcPerms("CREATE USER u1").restrictsAccess());
- }
+ /**
+ * Invalidate given role from CIDR permissions cache
+ * @param roleName role to invalidate
+ * @return returns true if given role found in the cache and invalidated, false otherwise
+ */
+ public boolean invalidateCidrPermissionsCache(String roleName);
}
diff --git a/src/java/org/apache/cassandra/auth/CassandraCIDRAuthorizer.java b/src/java/org/apache/cassandra/auth/CassandraCIDRAuthorizer.java
new file mode 100644
index 0000000000..ffbfdcadba
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/CassandraCIDRAuthorizer.java
@@ -0,0 +1,157 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+/**
+ * CassandraCIDRAuthorizer is backend for CIDR authorization checks
+ * 1, Provides functionality to populate Role to CIDR permisssions cache and
+ * 2, Uses CIDR groups mapping cache as backend to lookup CIDR groups of an IP
+ */
+public class CassandraCIDRAuthorizer extends AbstractCIDRAuthorizer
+{
+ private static final Logger logger = LoggerFactory.getLogger(AuthenticatedUser.class);
+ private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
+
+ protected static CIDRPermissionsCache cidrPermissionsCache;
+ protected static CIDRGroupsMappingCache cidrGroupsMappingCache;
+
+ @Override
+ public void setup()
+ {
+ commonSetup();
+
+ // Create <Role to CIDR permissions> cache
+ cidrPermissionsCache = new CIDRPermissionsCache(this::getCidrPermissionsForRole,
+ this.bulkLoadCidrPermsCache(),
+ this::requireAuthorization);
+
+ // Create CIDR groups cache
+ cidrGroupsMappingCache = new CIDRGroupsMappingCache(cidrGroupsMappingManager, cidrAuthorizerMetrics);
+ }
+
+ @Override
+ public void initCaches()
+ {
+ AuthCacheService.instance.register(cidrPermissionsCache);
+
+ // Load CIDR groups cache during the startup, to avoid increased latency during the first CIDR check
+ loadCidrGroupsCache();
+ }
+
+ private CIDRPermissions getCidrPermissionsForRole(RoleResource role)
+ {
+ return cidrPermissionsManager.getCidrPermissionsForRole(role);
+ }
+
+ private Supplier<Map<RoleResource, CIDRPermissions>> bulkLoadCidrPermsCache()
+ {
+ return cidrPermissionsManager.bulkLoader();
+ }
+
+ @Override
+ public boolean invalidateCidrPermissionsCache(String roleName)
+ {
+ if (roleName == null || roleName.isEmpty())
+ {
+ cidrPermissionsCache.invalidate();
+ return true;
+ }
+
+ return cidrPermissionsCache.invalidateCidrPermissions(roleName);
+ }
+
+ @Override
+ public void loadCidrGroupsCache()
+ {
+ cidrGroupsMappingCache.loadCidrGroupsCache();
+ }
+
+ @Override
+ public Set<String> lookupCidrGroupsForIp(InetAddress ip)
+ {
+ return cidrGroupsMappingCache.lookupCidrGroupsForIp(ip);
+ }
+
+ @VisibleForTesting
+ protected boolean isMonitorMode()
+ {
+ return DatabaseDescriptor.getCidrAuthorizerMode() == CIDRAuthorizerMode.MONITOR;
+ }
+
+ private boolean hasCidrAccess(RoleResource role, InetAddress ipAddress)
+ {
+ CIDRPermissions cidrPermissions = cidrPermissionsCache.get(role);
+ // Superusers and Roles without CIDR restrictions, should be able to access even when
+ // CIDR authorization is enabled and CIDR groups mapping table is not populated yet
+ if (!cidrPermissions.restrictsAccess() && !isMonitorMode())
+ return true;
+
+ Set<String> cidrGroups = lookupCidrGroupsForIp(ipAddress);
+
+ if (isMonitorMode())
+ {
+ if (cidrGroups != null && !cidrPermissions.canAccessFrom(cidrGroups))
+ noSpamLogger.warn("Role {} accessed from unauthorized IP {}, CIDR group {}", role.getRoleName(),
+ ipAddress.getHostAddress(), cidrGroups);
+ else
+ noSpamLogger.info("Role {} accessed from IP {}, CIDR group {}", role.getRoleName(),
+ ipAddress.getHostAddress(), cidrGroups);
+
+ cidrAuthorizerMetrics.incrAcceptedAccessCount(cidrGroups);
+ return true;
+ }
+
+ // Reach here only for enforce mode
+ if (cidrGroups == null || cidrGroups.isEmpty() || // No CIDR group found for this IP
+ !cidrPermissions.canAccessFrom(cidrGroups))
+ {
+ cidrAuthorizerMetrics.incrRejectedAccessCount(cidrGroups);
+ return false;
+ }
+
+ cidrAuthorizerMetrics.incrAcceptedAccessCount(cidrGroups);
+ return true;
+ }
+
+ @Override
+ public boolean hasAccessFromIp(RoleResource role, InetAddress ipAddress)
+ {
+ long startTimeNanos = MonotonicClock.Global.approxTime.now();
+
+ boolean hasAccess = hasCidrAccess(role, ipAddress);
+
+ cidrAuthorizerMetrics.cidrChecksLatency.update(MonotonicClock.Global.approxTime.now() - startTimeNanos,
+ TimeUnit.NANOSECONDS);
+ return hasAccess;
+ }
+}
diff --git a/src/java/org/apache/cassandra/auth/ICIDRAuthorizer.java b/src/java/org/apache/cassandra/auth/ICIDRAuthorizer.java
new file mode 100644
index 0000000000..737bbb220e
--- /dev/null
+++ b/src/java/org/apache/cassandra/auth/ICIDRAuthorizer.java
@@ -0,0 +1,112 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.util.Set;
+
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
+import org.apache.cassandra.utils.FBUtilities;
+
+/**
+ * Backend for CIDR authorization feature
+ */
+public interface ICIDRAuthorizer
+{
+ /**
+ * Supported modes by CIDR authorizer
+ */
+ public enum CIDRAuthorizerMode
+ {
+ MONITOR,
+ ENFORCE
+ }
+
+ public static ICIDRAuthorizer newCIDRAuthorizer(ParameterizedClass cidrAuthorizer)
+ {
+ if (cidrAuthorizer == null || cidrAuthorizer.class_name == null)
+ {
+ return new AllowAllCIDRAuthorizer();
+ }
+
+ String className = cidrAuthorizer.class_name;
+ if (!className.contains("."))
+ {
+ className = "org.apache.cassandra.auth." + className;
+ }
+ return FBUtilities.construct(className, "cidr authorizer");
+ }
+
+ public void setup();
+
+ /**
+ * Init caches held by CIDR authorizer
+ */
+ public void initCaches();
+
+ public CIDRGroupsMappingManager getCidrGroupsMappingManager();
+
+ public CIDRAuthorizerMetrics getCidrAuthorizerMetrics();
+
+ public boolean requireAuthorization();
+
+ /**
+ * Set CIDR permissions for a given role
+ * @param role role for which to set CIDR permissions
+ * @param cidrPermissions CIR permissions to set for the role
+ */
+ public void setCidrGroupsForRole(RoleResource role, CIDRPermissions cidrPermissions);
+
+ /**
+ * Drop CIDR permissions of a role
+ * @param role for which to drop cidr permissions
+ */
+ public void dropCidrPermissionsForRole(RoleResource role);
+
+ /**
+ * Invalidate given role from CIDR permissions cache
+ * @param roleName role to invalidate
+ * @return returns true if given role found in the cache and invalidated, false otherwise
+ */
+ public boolean invalidateCidrPermissionsCache(String roleName);
+
+ public void validateConfiguration() throws ConfigurationException;
+
+ /**
+ * Load CIDR groups mapping cache
+ */
+ public void loadCidrGroupsCache();
+
+ /**
+ * Lookup IP in CIDR groups mapping cache
+ * @param ip input IP to lookup CIDR group
+ * @return returns best matching CIDR group for this IP
+ */
+ public Set<String> lookupCidrGroupsForIp(InetAddress ip);
+
+ /**
+ * Determines does the given role has access from CIDR groups associated with given IP
+ * @param role role to check access
+ * @param ipAddress IP of the client
+ * @return returns true if role has access from given IP, false otherwise
+ */
+ public boolean hasAccessFromIp(RoleResource role, InetAddress ipAddress);
+}
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 5e904bbf3e..34a173c8c6 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -82,6 +82,8 @@ public class Config
public String role_manager;
public ParameterizedClass crypto_provider;
public String network_authorizer;
+ public ParameterizedClass cidr_authorizer;
+
@Replaces(oldName = "permissions_validity_in_ms", converter = Converters.MILLIS_DURATION_INT, deprecated = true)
public volatile DurationSpec.IntMillisecondsBound permissions_validity = new DurationSpec.IntMillisecondsBound("2s");
public volatile int permissions_cache_max_entries = 1000;
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 88c0ab9e7d..91a55aac7d 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -68,6 +68,7 @@ import org.apache.cassandra.auth.AllowAllInternodeAuthenticator;
import org.apache.cassandra.auth.AuthConfig;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IAuthorizer;
+import org.apache.cassandra.auth.ICIDRAuthorizer;
import org.apache.cassandra.auth.IInternodeAuthenticator;
import org.apache.cassandra.auth.INetworkAuthorizer;
import org.apache.cassandra.auth.IRoleManager;
@@ -181,6 +182,8 @@ public class DatabaseDescriptor
private static IAuthenticator authenticator;
private static IAuthorizer authorizer;
private static INetworkAuthorizer networkAuthorizer;
+ private static ICIDRAuthorizer cidrAuthorizer;
+
// Don't initialize the role manager until applying config. The options supported by CassandraRoleManager
// depend on the configured IAuthenticator, so defer creating it until that's been set.
private static IRoleManager roleManager;
@@ -1584,6 +1587,72 @@ public class DatabaseDescriptor
DatabaseDescriptor.networkAuthorizer = networkAuthorizer;
}
+ public static ICIDRAuthorizer getCIDRAuthorizer()
+ {
+ return cidrAuthorizer;
+ }
+
+ public static void setCIDRAuthorizer(ICIDRAuthorizer cidrAuthorizer)
+ {
+ DatabaseDescriptor.cidrAuthorizer = cidrAuthorizer;
+ }
+
+ public static boolean getCidrChecksForSuperusers()
+ {
+ boolean defaultCidrChecksForSuperusers = false;
+
+ if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null)
+ return defaultCidrChecksForSuperusers;
+
+ String value = conf.cidr_authorizer.parameters.get("cidr_checks_for_superusers");
+ if (value == null || value.isEmpty())
+ return defaultCidrChecksForSuperusers;
+
+ return Boolean.parseBoolean(value);
+ }
+
+ public static ICIDRAuthorizer.CIDRAuthorizerMode getCidrAuthorizerMode()
+ {
+ ICIDRAuthorizer.CIDRAuthorizerMode defaultCidrAuthorizerMode = ICIDRAuthorizer.CIDRAuthorizerMode.MONITOR;
+
+ if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null)
+ return defaultCidrAuthorizerMode;
+
+ String cidrAuthorizerMode = conf.cidr_authorizer.parameters.get("cidr_authorizer_mode");
+ if (cidrAuthorizerMode == null || cidrAuthorizerMode.isEmpty())
+ return defaultCidrAuthorizerMode;
+
+ return ICIDRAuthorizer.CIDRAuthorizerMode.valueOf(cidrAuthorizerMode.toUpperCase());
+ }
+
+ public static int getCidrGroupsCacheRefreshInterval()
+ {
+ int defaultCidrGroupsCacheRefreshInterval = 5; // mins
+
+ if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null)
+ return defaultCidrGroupsCacheRefreshInterval;
+
+ String cidrGroupsCacheRefreshInterval = conf.cidr_authorizer.parameters.get("cidr_groups_cache_refresh_interval");
+ if (cidrGroupsCacheRefreshInterval == null || cidrGroupsCacheRefreshInterval.isEmpty())
+ return defaultCidrGroupsCacheRefreshInterval;
+
+ return Integer.parseInt(cidrGroupsCacheRefreshInterval);
+ }
+
+ public static int getIpCacheMaxSize()
+ {
+ int defaultIpCacheMaxSize = 100;
+
+ if (conf.cidr_authorizer == null || conf.cidr_authorizer.parameters == null)
+ return defaultIpCacheMaxSize;
+
+ String ipCacheMaxSize = conf.cidr_authorizer.parameters.get("ip_cache_max_size");
+ if (ipCacheMaxSize == null || ipCacheMaxSize.isEmpty())
+ return defaultIpCacheMaxSize;
+
+ return Integer.parseInt(ipCacheMaxSize);
+ }
+
public static void setAuthFromRoot(boolean fromRoot)
{
conf.traverse_auth_from_root = fromRoot;
diff --git a/src/java/org/apache/cassandra/cql3/CIDR.java b/src/java/org/apache/cassandra/cql3/CIDR.java
new file mode 100644
index 0000000000..7da0981535
--- /dev/null
+++ b/src/java/org/apache/cassandra/cql3/CIDR.java
@@ -0,0 +1,270 @@
+/*
+ * 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.cql3;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+import org.apache.cassandra.utils.Pair;
+
+/**
+ * Contains a CIDR, and operations on it
+ */
+public final class CIDR
+{
+ private final InetAddress startIpAddress;
+ private final InetAddress endIpAddress;
+ // max mask value with IPv6 is 128, not easy to handle value 128 using Byte, hence using short
+ private final short netMask;
+
+ /**
+ * Generates a CIDR from given IP and netmask
+ * @param ipAddress IP address of CIDR
+ * @param netMask netmask of CIDR
+ */
+ public CIDR(InetAddress ipAddress, short netMask)
+ {
+ if (netMask > maxNetMaskAllowed(ipAddress))
+ throw new IllegalArgumentException("Invalid netmask " + netMask + " for IP " + ipAddress.getHostAddress());
+
+ Pair<InetAddress, InetAddress> ipRange = calcIpRangeOfCidr(ipAddress, netMask);
+ this.startIpAddress = ipRange.left();
+ this.endIpAddress = ipRange.right();
+ this.netMask = netMask;
+ }
+
+ /**
+ * Generates a CIDR from given string
+ * @param cidrStr CIDR as string
+ */
+ public static CIDR getInstance(String cidrStr)
+ {
+ if (cidrStr == null || cidrStr.isEmpty())
+ {
+ throw new IllegalArgumentException(String.format("%s is not a valid CIDR String", cidrStr));
+ }
+
+ String[] parts = cidrStr.split("/");
+ if (parts.length != 2)
+ {
+ throw new IllegalArgumentException(String.format("%s is not a valid CIDR String", cidrStr));
+ }
+
+ short netMask = Short.parseShort(parts[1]);
+
+ InetAddress ipAddress;
+ try
+ {
+ ipAddress = InetAddress.getByName(parts[0]);
+ if (ipAddress instanceof Inet4Address && parts[0].contains(":") && parts[0].contains("."))
+ {
+ // Input string is in IPv4 mapped IPv6 format. InetAddress converted it to IPv4
+ // So adjust the net mask accordingly
+ // example, 0:0:0:0:0:ffff:192.1.56.10/96 would be converted to 192.1.56.10/0
+ netMask -= 96; // 6 * 16 bits
+ }
+ }
+ catch (UnknownHostException e)
+ {
+ throw new IllegalArgumentException(String.format("%s is not a valid CIDR String", cidrStr));
+ }
+
+ short maxMaskValue = maxNetMaskAllowed(ipAddress);
+ if (netMask < 0 || netMask > maxMaskValue)
+ {
+ throw new IllegalArgumentException(String.format("%s is not a valid CIDR String", cidrStr));
+ }
+
+ return new CIDR(ipAddress, netMask);
+ }
+
+ private static short maxNetMaskAllowed(InetAddress ipAddress)
+ {
+ if (ipAddress instanceof Inet6Address)
+ return 128;
+
+ return 32;
+ }
+
+ /**
+ * This function calculates starting and ending IP of a CIDR.
+ * For example, for CIDR 10.20.33.40/24, starting IP is 10.20.33.0, ending IP is 10.20.33.255
+ * @param ipAddress IP address of the CIDR
+ * @param netMask net mask of the CIDR
+ * @return pair of starting and ending IP of the CIDR
+ */
+ private static Pair<InetAddress, InetAddress> calcIpRangeOfCidr(InetAddress ipAddress, short netMask)
+ {
+ if (netMask > maxNetMaskAllowed(ipAddress))
+ throw new IllegalArgumentException("Invalid netmask " + netMask + " for IP " + ipAddress.getHostAddress());
+
+ // Starting and ending IP are same as CIDR's IP if net mask is 32 for IPv4, 128 for IPv6
+ if (netMask == maxNetMaskAllowed(ipAddress))
+ return Pair.create(ipAddress, ipAddress);
+
+ byte[] startIpBytes = ipAddress.getAddress();
+ byte[] endIpBytes = ipAddress.getAddress();
+
+ // netmask indicates number of bits to remain unchanged. Let's call that as netmask bit.
+ // Calculate the offset of the byte where netmask bit ends
+ int byteOffset = netMask / Byte.SIZE;
+
+ // Calculate the bit number within that byte, where netmask bit ends
+ int bitOffset = netMask % Byte.SIZE;
+
+ // In that byte, set bits after netmask bit to 0 for starting IP, to 1 for ending IP,
+ // keeping bits before including netmask bit as it is
+ int unsignedByte = Byte.toUnsignedInt(startIpBytes[byteOffset]);
+ startIpBytes[byteOffset] = (byte) (unsignedByte & (0xff << (Byte.SIZE - bitOffset)));
+ endIpBytes[byteOffset] = (byte) (unsignedByte | (0xFF >>> bitOffset));
+
+ // set all remaining bits after netmask to 0
+ for (byteOffset += 1; byteOffset < startIpBytes.length; byteOffset++)
+ {
+ startIpBytes[byteOffset] = 0;
+ endIpBytes[byteOffset] = (byte) 0xFF;
+ }
+
+ try
+ {
+ return Pair.create(InetAddress.getByAddress(startIpBytes), InetAddress.getByAddress(endIpBytes));
+ }
+ catch (UnknownHostException e)
+ {
+ throw new IllegalStateException("Invalid bytes for constructing IP", e);
+ }
+ }
+
+ /**
+ * Get starting IP of the CIDR
+ * @return returns IP address
+ */
+ public InetAddress getStartIpAddress()
+ {
+ return startIpAddress;
+ }
+
+ /**
+ * Get ending IP of the CIDR
+ * @return returns IP address
+ */
+ public InetAddress getEndIpAddress()
+ {
+ return endIpAddress;
+ }
+
+ /**
+ * Get netmask of the CIDR
+ * @return returns netmask as short
+ */
+ public short getNetMask()
+ {
+ return netMask;
+ }
+
+ /**
+ * Tells is this IPv4 format CIDR
+ * @return true if IPv4 CIDR, otherwise false
+ */
+ public boolean isIPv4()
+ {
+ return (startIpAddress instanceof Inet4Address);
+ }
+
+ /**
+ * Tells is this IPv6 format CIDR
+ * @return true if IPv6 CIDR, otherwise false
+ */
+ public boolean isIPv6()
+ {
+ return (startIpAddress instanceof Inet6Address);
+ }
+
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ CIDR cidr = (CIDR) o;
+ return netMask == cidr.netMask &&
+ startIpAddress.equals(cidr.startIpAddress) &&
+ endIpAddress.equals(cidr.endIpAddress);
+ }
+
+ public int hashCode()
+ {
+ return Objects.hash(startIpAddress, endIpAddress, netMask);
+ }
+
+ /**
+ * Constructs CIDR as string
+ * @return returns CIDR as string
+ */
+ @Override
+ public String toString()
+ {
+ return this.startIpAddress.getHostAddress() + '/' + this.netMask;
+ }
+
+ public String asCqlTupleString()
+ {
+ return "('" + this.startIpAddress.getHostAddress() + "', " + this.netMask + ')';
+ }
+
+ /**
+ * Compare 2 IpAddresses objects lexicographically
+ * @return true if IP ranges overlap with each other; otherwise, return false
+ */
+ public static int compareIPs(InetAddress l, InetAddress r)
+ {
+ byte[] lBytes = l.getAddress();
+ byte[] rBytes = r.getAddress();
+
+ for (int i = 0; i < lBytes.length; i++)
+ {
+ int comp = Byte.toUnsignedInt(lBytes[i]) - Byte.toUnsignedInt(rBytes[i]);
+ if (comp != 0)
+ {
+ return comp;
+ }
+ }
+ return 0;
+ }
+
+ public static boolean overlaps(CIDR left, CIDR right)
+ {
+ // Sort IP ranges by the start address
+ CIDR lower = left;
+ CIDR higher = right;
+
+ if (compareIPs(left.startIpAddress, right.startIpAddress) > 0)
+ {
+ lower = right;
+ higher = left;
+ }
+
+ // Overlaps when lower end is >= to higher start address
+ return compareIPs(lower.endIpAddress, higher.startIpAddress) >= 0;
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
index eb0e3e01f2..aadf6c788d 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
@@ -36,18 +36,21 @@ public class AlterRoleStatement extends AuthenticationStatement
private final RoleResource role;
private final RoleOptions opts;
final DCPermissions dcPermissions;
+ final CIDRPermissions cidrPermissions;
private final boolean ifExists;
public AlterRoleStatement(RoleName name, RoleOptions opts)
{
- this(name, opts, null, false);
+ this(name, opts, null, null, false);
}
- public AlterRoleStatement(RoleName name, RoleOptions opts, DCPermissions dcPermissions, boolean ifExists)
+ public AlterRoleStatement(RoleName name, RoleOptions opts, DCPermissions dcPermissions,
+ CIDRPermissions cidrPermissions, boolean ifExists)
{
this.role = RoleResource.role(name.getName());
this.opts = opts;
this.dcPermissions = dcPermissions;
+ this.cidrPermissions = cidrPermissions;
this.ifExists = ifExists;
}
@@ -55,13 +58,19 @@ public class AlterRoleStatement extends AuthenticationStatement
{
opts.validate();
+ if (opts.isEmpty() && dcPermissions == null && cidrPermissions == null)
+ throw new InvalidRequestException("ALTER [ROLE|USER] can't be empty");
+
if (dcPermissions != null)
{
dcPermissions.validate();
}
- if (opts.isEmpty() && dcPermissions == null)
- throw new InvalidRequestException("ALTER [ROLE|USER] can't be empty");
+ if (cidrPermissions != null)
+ {
+ // Ensure input CIDR group names are valid, i.e, existing in CIDR groups mapping table
+ cidrPermissions.validate();
+ }
// validate login here before authorize to avoid leaking user existence to anonymous users.
state.ensureNotAnonymous();
@@ -107,8 +116,13 @@ public class AlterRoleStatement extends AuthenticationStatement
{
if (!opts.isEmpty())
DatabaseDescriptor.getRoleManager().alterRole(state.getUser(), role, opts);
+
if (dcPermissions != null)
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
+
+ if (cidrPermissions != null)
+ DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions);
+
return null;
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java
index 04f183d7bb..d6e0a1298c 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java
@@ -34,13 +34,16 @@ public class CreateRoleStatement extends AuthenticationStatement
private final RoleResource role;
private final RoleOptions opts;
final DCPermissions dcPermissions;
+ final CIDRPermissions cidrPermissions;
private final boolean ifNotExists;
- public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions, boolean ifNotExists)
+ public CreateRoleStatement(RoleName name, RoleOptions options, DCPermissions dcPermissions,
+ CIDRPermissions cidrPermissions, boolean ifNotExists)
{
this.role = RoleResource.role(name.getName());
this.opts = options;
this.dcPermissions = dcPermissions;
+ this.cidrPermissions = cidrPermissions;
this.ifNotExists = ifNotExists;
}
@@ -58,13 +61,18 @@ public class CreateRoleStatement extends AuthenticationStatement
{
opts.validate();
+ if (role.getRoleName().isEmpty())
+ throw new InvalidRequestException("Role name can't be an empty string");
+
if (dcPermissions != null)
{
dcPermissions.validate();
}
- if (role.getRoleName().isEmpty())
- throw new InvalidRequestException("Role name can't be an empty string");
+ if (cidrPermissions != null)
+ {
+ cidrPermissions.validate();
+ }
// validate login here before authorize to avoid leaking role existence to anonymous users.
state.ensureNotAnonymous();
@@ -84,6 +92,10 @@ public class CreateRoleStatement extends AuthenticationStatement
{
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
}
+
+ if (cidrPermissions != null)
+ DatabaseDescriptor.getCIDRAuthorizer().setCidrGroupsForRole(role, cidrPermissions);
+
grantPermissionsToCreator(state);
return null;
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java
index 058ab01c04..13ba54a52d 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropRoleStatement.java
@@ -75,6 +75,7 @@ public class DropRoleStatement extends AuthenticationStatement
DatabaseDescriptor.getAuthorizer().revokeAllFrom(role);
DatabaseDescriptor.getAuthorizer().revokeAllOn(role);
DatabaseDescriptor.getNetworkAuthorizer().drop(role);
+ DatabaseDescriptor.getCIDRAuthorizer().dropCidrPermissionsForRole(role);
return null;
}
diff --git a/src/java/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTable.java b/src/java/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTable.java
new file mode 100644
index 0000000000..580dcdd1fd
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTable.java
@@ -0,0 +1,256 @@
+/*
+ * 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.virtual;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Snapshot;
+import org.apache.cassandra.auth.CassandraAuthorizer;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.marshal.DoubleType;
+import org.apache.cassandra.db.marshal.LongType;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.dht.LocalPartitioner;
+import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.transport.messages.ResultMessage;
+import org.apache.cassandra.utils.MBeanWrapper;
+
+import static org.apache.cassandra.service.QueryState.forInternalCalls;
+import static org.apache.cassandra.utils.Clock.Global.nanoTime;
+
+/**
+ * Virtual tables capturing metrics related to CIDR filtering
+ */
+public class CIDRFilteringMetricsTable implements CIDRFilteringMetricsTableMBean
+{
+ public static final String MBEAN_NAME = "org.apache.cassandra.db:type=CIDRFilteringMetricsTable";
+
+ private static final CIDRFilteringMetricsTable instance = new CIDRFilteringMetricsTable();
+
+ CIDRFilteringMetricsTable()
+ {
+ // Register mbean for nodetool to access vtable entries
+ if (!MBeanWrapper.instance.isRegistered(MBEAN_NAME))
+ MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
+ }
+
+ public static Collection<VirtualTable> getAll(String keyspace)
+ {
+ return Arrays.asList(
+ new CIDRFilteringMetricsCountsTable(keyspace),
+ new CIDRFilteringMetricsLatenciesTable(keyspace)
+ );
+ }
+
+ /**
+ * Virtual table capturing counts i.e, non-latency metrics related to CIDR filtering
+ */
+ public static class CIDRFilteringMetricsCountsTable extends AbstractVirtualTable
+ {
+ public static final String TABLE_NAME = "cidr_filtering_metrics_counts";
+
+ public static final String NAME_COL = "name";
+ public static final String VALUE_COL = "value";
+ public static final String CIDR_ACCESSES_ACCEPTED_COUNT_NAME_PREFIX = "Number of CIDR accesses accepted from CIDR group - ";
+ public static final String CIDR_ACCESSES_REJECTED_COUNT_NAME_PREFIX = "Number of CIDR accesses rejected from CIDR group - ";
+ public static final String CIDR_GROUPS_CACHE_RELOAD_COUNT_NAME = "CIDR groups cache reload count";
+
+ @VisibleForTesting
+ CIDRFilteringMetricsCountsTable(String keyspace)
+ {
+ super(TableMetadata.builder(keyspace, TABLE_NAME)
+ .comment("Count metrics specific to CIDR filtering")
+ .kind(TableMetadata.Kind.VIRTUAL)
+ .partitioner(new LocalPartitioner(UTF8Type.instance))
+ .addPartitionKeyColumn(NAME_COL, UTF8Type.instance)
+ .addRegularColumn(VALUE_COL, LongType.instance)
+ .build());
+ }
+
+ private void addRow(SimpleDataSet dataSet, String name, long value)
+ {
+ dataSet.row(name)
+ .column(VALUE_COL, value);
+ }
+
+ @Override
+ public DataSet data()
+ {
+ SimpleDataSet result = new SimpleDataSet(metadata());
+
+ CIDRAuthorizerMetrics cidrAuthorizerMetrics = DatabaseDescriptor.getCIDRAuthorizer().getCidrAuthorizerMetrics();
+
+ for (Map.Entry<String, Counter> entry : cidrAuthorizerMetrics.acceptedCidrAccessCount.entrySet())
+ {
+ addRow(result, CIDR_ACCESSES_ACCEPTED_COUNT_NAME_PREFIX + entry.getKey(),
+ entry.getValue().getCount());
+ }
+
+ for (Map.Entry<String, Counter> entry : cidrAuthorizerMetrics.rejectedCidrAccessCount.entrySet())
+ {
+ addRow(result, CIDR_ACCESSES_REJECTED_COUNT_NAME_PREFIX + entry.getKey(),
+ entry.getValue().getCount());
+ }
+
+ addRow(result, CIDR_GROUPS_CACHE_RELOAD_COUNT_NAME, cidrAuthorizerMetrics.cacheReloadCount.getCount());
+
+ return result;
+ }
+ }
+
+ /**
+ * Virtual table capturing latency metrics related to CIDR filtering
+ */
+ public static class CIDRFilteringMetricsLatenciesTable extends AbstractVirtualTable
+ {
+ public static final String TABLE_NAME = "cidr_filtering_metrics_latencies";
+
+ public static final String NAME_COL = "name";
+ public static final String P50_COL = "p50th";
+ public static final String P95_COL = "p95th";
+ public static final String P99_COL = "p99th";
+ public static final String P999_COL = "p999th";
+ public static final String MAX_COL = "max";
+
+ public static final String CIDR_CHECKS_LATENCY_NAME = "CIDR checks latency (ns)";
+ public static final String CIDR_GROUPS_CACHE_RELOAD_LATENCY_NAME = "CIDR groups cache reload latency (ns)";
+ public static final String LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY_NAME = "Lookup IP in CIDR groups cache latency (ns)";
+
+ @VisibleForTesting
+ CIDRFilteringMetricsLatenciesTable(String keyspace)
+ {
+ super(TableMetadata.builder(keyspace, TABLE_NAME)
+ .comment("Latency metrics specific to CIDR filtering")
+ .kind(TableMetadata.Kind.VIRTUAL)
+ .partitioner(new LocalPartitioner(UTF8Type.instance))
+ .addPartitionKeyColumn(NAME_COL, UTF8Type.instance)
+ .addRegularColumn(P50_COL, DoubleType.instance)
+ .addRegularColumn(P95_COL, DoubleType.instance)
+ .addRegularColumn(P99_COL, DoubleType.instance)
+ .addRegularColumn(P999_COL, DoubleType.instance)
+ .addRegularColumn(MAX_COL, DoubleType.instance)
+ .build());
+ }
+
+ private void addRow(SimpleDataSet dataSet, String name, Snapshot snapshot)
+ {
+ dataSet.row(name)
+ .column(P50_COL, snapshot.getMedian())
+ .column(P95_COL, snapshot.get95thPercentile())
+ .column(P99_COL, snapshot.get99thPercentile())
+ .column(P999_COL, snapshot.get999thPercentile())
+ .column(MAX_COL, (double) snapshot.getMax());
+ }
+
+ @Override
+ public DataSet data()
+ {
+ SimpleDataSet result = new SimpleDataSet(metadata());
+
+ CIDRAuthorizerMetrics cidrAuthorizerMetrics =
+ DatabaseDescriptor.getCIDRAuthorizer().getCidrAuthorizerMetrics();
+
+ addRow(result, CIDR_CHECKS_LATENCY_NAME, cidrAuthorizerMetrics.cidrChecksLatency.getSnapshot());
+ addRow(result, CIDR_GROUPS_CACHE_RELOAD_LATENCY_NAME,
+ cidrAuthorizerMetrics.cacheReloadLatency.getSnapshot());
+ addRow(result, LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY_NAME,
+ cidrAuthorizerMetrics.lookupCidrGroupsForIpLatency.getSnapshot());
+
+ return result;
+ }
+ }
+
+ private UntypedResultSet retrieveRows(SelectStatement statement)
+ {
+ QueryOptions options = QueryOptions.forInternalCalls(CassandraAuthorizer.authReadConsistencyLevel(),
+ Collections.emptyList());
+
+ ResultMessage.Rows rows = statement.execute(forInternalCalls(), options, nanoTime());
+ return UntypedResultSet.create(rows.result);
+ }
+
+ public Map<String, Long> getCountsMetricsFromVtable()
+ {
+ String countsMetricsTableName = SchemaConstants.VIRTUAL_VIEWS + '.' +
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.TABLE_NAME;
+
+ SelectStatement getCountsMetricsStatement =
+ (SelectStatement) QueryProcessor.getStatement(String.format("SELECT * FROM %s", countsMetricsTableName),
+ ClientState.forInternalCalls());
+
+ Map<String, Long> metrics = new HashMap<>();
+
+ UntypedResultSet result = retrieveRows(getCountsMetricsStatement);
+ for (UntypedResultSet.Row row : result)
+ {
+ if (!row.has(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.NAME_COL) ||
+ !row.has(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.VALUE_COL))
+ throw new RuntimeException("Invalid row " + row + " in table: " + countsMetricsTableName);
+
+ metrics.put(row.getString(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.NAME_COL),
+ row.getLong(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.VALUE_COL));
+ }
+
+ return metrics;
+ }
+
+ public Map<String, List<Double>> getLatenciesMetricsFromVtable()
+ {
+ String latenciesMetricsTableName = SchemaConstants.VIRTUAL_VIEWS + '.' +
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.TABLE_NAME;
+
+ SelectStatement getLatenciesMetricsStatement =
+ (SelectStatement) QueryProcessor.getStatement(String.format("SELECT * FROM %s", latenciesMetricsTableName),
+ ClientState.forInternalCalls());
+
+ Map<String, List<Double>> metrics = new HashMap<>();
+
+ UntypedResultSet result = retrieveRows(getLatenciesMetricsStatement);
+ for (UntypedResultSet.Row row : result)
+ {
+ if (!row.has(CIDRFilteringMetricsLatenciesTable.NAME_COL) ||
+ !row.has(CIDRFilteringMetricsLatenciesTable.P50_COL))
+ throw new RuntimeException("Invalid row " + row + " in table: " + latenciesMetricsTableName);
+
+ metrics.put(row.getString(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.NAME_COL),
+ Arrays.asList(row.getDouble(CIDRFilteringMetricsLatenciesTable.P50_COL),
+ row.getDouble(CIDRFilteringMetricsLatenciesTable.P95_COL),
+ row.getDouble(CIDRFilteringMetricsLatenciesTable.P99_COL),
+ row.getDouble(CIDRFilteringMetricsLatenciesTable.P999_COL),
+ row.getDouble(CIDRFilteringMetricsLatenciesTable.MAX_COL)));
+ }
+
+ return metrics;
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java b/src/java/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableMBean.java
similarity index 51%
copy from test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
copy to src/java/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableMBean.java
index 51c61bbb12..b90ed3c818 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
+++ b/src/java/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableMBean.java
@@ -16,32 +16,25 @@
* limitations under the License.
*/
-package org.apache.cassandra.cql3.statements;
+package org.apache.cassandra.db.virtual;
-import org.junit.Assert;
-import org.junit.Test;
+import java.util.List;
+import java.util.Map;
-import org.apache.cassandra.auth.DCPermissions;
-import org.apache.cassandra.cql3.CQLStatement;
-import org.apache.cassandra.cql3.QueryProcessor;
-
-public class CreateUserStatementTest
+/**
+ * mbean to expose CIDR filtering metrics virtual table to JMX/nodetool
+ */
+public interface CIDRFilteringMetricsTableMBean
{
- private static CreateRoleStatement parse(String query)
- {
- CQLStatement.Raw stmt = QueryProcessor.parseStatement(query);
- Assert.assertTrue(stmt instanceof CreateRoleStatement);
- return (CreateRoleStatement) stmt;
- }
-
- private static DCPermissions dcPerms(String query)
- {
- return parse(query).dcPermissions;
- }
+ /**
+ * Retreive rows from CIDR filtering counts metrics virtual table
+ * @return map of rows, map's key is metric name, map's value is metric value
+ */
+ public Map<String, Long> getCountsMetricsFromVtable();
- @Test
- public void allDcsImplicit() throws Exception
- {
- Assert.assertFalse(dcPerms("CREATE USER u1").restrictsAccess());
- }
+ /**
+ * Retreive rows from CIDR filtering latency metrics virtual table
+ * @return map of rows, map's key is metric name, map's values are latency percentiles
+ */
+ public Map<String, List<Double>> getLatenciesMetricsFromVtable();
}
diff --git a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
index 6a718c7145..7d6152bdc2 100644
--- a/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
+++ b/src/java/org/apache/cassandra/db/virtual/SystemViewsKeyspace.java
@@ -53,6 +53,7 @@ public final class SystemViewsKeyspace extends VirtualKeyspace
.add(new LogMessagesTable(VIRTUAL_VIEWS))
.add(new SnapshotsTable(VIRTUAL_VIEWS))
.addAll(LocalRepairTables.getAll(VIRTUAL_VIEWS))
+ .addAll(CIDRFilteringMetricsTable.getAll(VIRTUAL_VIEWS))
.addAll(StorageAttachedIndexTables.getAll(VIRTUAL_VIEWS))
.build());
}
diff --git a/src/java/org/apache/cassandra/metrics/CIDRAuthorizerMetrics.java b/src/java/org/apache/cassandra/metrics/CIDRAuthorizerMetrics.java
new file mode 100644
index 0000000000..0de51dfb09
--- /dev/null
+++ b/src/java/org/apache/cassandra/metrics/CIDRAuthorizerMetrics.java
@@ -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.metrics;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Timer;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Captures metrics of CIDR authorizer
+ */
+public class CIDRAuthorizerMetrics
+{
+ private static final MetricNameFactory factory = new DefaultNameFactory("CIDRAuthorizer");
+
+ // Number of user accesses rejected by CIDR authorization
+ public static final String CIDR_ACCESSES_REJECTED_COUNT_PREFIX = "CIDRAccessesRejectedCount - ";
+ public static final String CIDR_ACCESSES_ACCEPTED_COUNT_PREFIX = "CIDRAccessesAcceptedCount - ";
+ // Time taken by CIDR authorization checks
+ public static final String CIDR_CHECKS_LATENCY = "CIDRChecksLatency";
+
+ // Number of times CIDR groups cache was (re)loaded
+ public static final String CIDR_GROUPS_CACHE_RELOAD_COUNT = "CIDRGroupsMappingCacheReloadCount";
+ // Time taken to reload CIDR groups cache
+ public static final String CIDR_GROUPS_CACHE_RELOAD_LATENCY = "CIDRGroupsMappingCacheReloadLatency";
+ // Time taken to look up an IP in CIDR groups cache, to find best matching CIDR group
+ public static final String LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY = "LookupCIDRGroupsForIPLatency";
+
+ public final Timer cidrChecksLatency;
+ public final Counter cacheReloadCount;
+ public final Timer cacheReloadLatency;
+ public final Timer lookupCidrGroupsForIpLatency;
+
+ public final ConcurrentHashMap<String, Counter> rejectedCidrAccessCount = new ConcurrentHashMap<>();
+ public final ConcurrentHashMap<String, Counter> acceptedCidrAccessCount = new ConcurrentHashMap<>();
+
+ public CIDRAuthorizerMetrics()
+ {
+ cidrChecksLatency = Metrics.timer(factory.createMetricName(CIDR_CHECKS_LATENCY));
+ cacheReloadCount = Metrics.counter(factory.createMetricName(CIDR_GROUPS_CACHE_RELOAD_COUNT));
+ cacheReloadLatency = Metrics.timer(factory.createMetricName(CIDR_GROUPS_CACHE_RELOAD_LATENCY));
+ lookupCidrGroupsForIpLatency = Metrics.timer(factory.createMetricName(LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY));
+ }
+
+ private void incCounter(ConcurrentHashMap<String, Counter> metric, String metricPrefix, String cidrGroup)
+ {
+ metric.computeIfAbsent(cidrGroup,
+ k -> Metrics.counter(factory.createMetricName(metricPrefix + cidrGroup))).inc();
+ }
+
+ public void incrRejectedAccessCount(Set<String> cidrGroups)
+ {
+ if (cidrGroups == null || cidrGroups.isEmpty())
+ {
+ incCounter(rejectedCidrAccessCount, CIDR_ACCESSES_REJECTED_COUNT_PREFIX, "Undefined");
+ return;
+ }
+
+ for (String cidrGroup : cidrGroups)
+ {
+ incCounter(rejectedCidrAccessCount, CIDR_ACCESSES_REJECTED_COUNT_PREFIX, cidrGroup);
+ }
+ }
+
+ public void incrAcceptedAccessCount(Set<String> cidrGroups)
+ {
+ if (cidrGroups == null || cidrGroups.isEmpty())
+ {
+ incCounter(acceptedCidrAccessCount, CIDR_ACCESSES_ACCEPTED_COUNT_PREFIX, "Undefined");
+ return;
+ }
+
+ for (String cidrGroup : cidrGroups)
+ {
+ incCounter(acceptedCidrAccessCount, CIDR_ACCESSES_ACCEPTED_COUNT_PREFIX, cidrGroup);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java
index f03a9712e8..d9a0dbbc9e 100644
--- a/src/java/org/apache/cassandra/service/ClientState.java
+++ b/src/java/org/apache/cassandra/service/ClientState.java
@@ -534,6 +534,11 @@ public class ClientState
{
throw new UnauthorizedException(String.format("You do not have access to this datacenter (%s)", Datacenters.thisDatacenter()));
}
+ else
+ {
+ if (remoteAddress != null && !user.hasAccessFromIp(remoteAddress))
+ throw new UnauthorizedException("You do not have access from this IP " + remoteAddress.getHostString());
+ }
}
public void ensureNotAnonymous()
diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java
index c3c5fbba09..28a8c80d09 100644
--- a/src/java/org/apache/cassandra/service/StorageService.java
+++ b/src/java/org/apache/cassandra/service/StorageService.java
@@ -128,11 +128,12 @@ import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
import org.apache.cassandra.dht.BootStrapper;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token.TokenFactory;
+import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.dht.RangeStreamer;
import org.apache.cassandra.dht.RangeStreamer.FetchReplica;
import org.apache.cassandra.dht.StreamStateStore;
import org.apache.cassandra.dht.Token;
-import org.apache.cassandra.dht.Token.TokenFactory;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.UnavailableException;
@@ -147,7 +148,6 @@ import org.apache.cassandra.gms.IEndpointStateChangeSubscriber;
import org.apache.cassandra.gms.IFailureDetector;
import org.apache.cassandra.gms.TokenSerializer;
import org.apache.cassandra.gms.VersionedValue;
-import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.index.IndexStatusManager;
import org.apache.cassandra.io.sstable.IScrubber;
import org.apache.cassandra.io.sstable.IVerifier;
@@ -1374,6 +1374,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
DatabaseDescriptor.getNetworkAuthorizer().setup();
+ DatabaseDescriptor.getCIDRAuthorizer().setup();
AuthCacheService.initializeAndRegisterCaches();
Schema.instance.registerListener(new AuthSchemaChangeListener());
authSetupComplete = true;
diff --git a/src/java/org/apache/cassandra/tools/ListCIDRGroups.java b/src/java/org/apache/cassandra/tools/ListCIDRGroups.java
new file mode 100644
index 0000000000..4cc3097512
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/ListCIDRGroups.java
@@ -0,0 +1,52 @@
+/*
+ * 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.tools;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.airlift.airline.Arguments;
+import io.airlift.airline.Command;
+import org.apache.cassandra.auth.AuthKeyspace;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+/**
+ * Nodetool command to list available CIDR groups, in the table {@link AuthKeyspace#CIDR_GROUPS}
+ */
+@Command(name = "listcidrgroups", description = "List existing cidr groups")
+public class ListCIDRGroups extends NodeToolCmd
+{
+ @Arguments(usage = "[<cidrGroup>]", description = "LIST operation can be invoked with or without cidr group name")
+ private List<String> args = new ArrayList<>();
+
+ @Override
+ public void execute(NodeProbe probe)
+ {
+ PrintStream out = probe.output().out;
+
+ if (args.size() < 1)
+ {
+ probe.printSet(out, "CIDR Groups", probe.listAvailableCidrGroups());
+ return;
+ }
+
+ String cidrGroup = args.get(0);
+ probe.printSet(out, "CIDRs", probe.listCidrsOfCidrGroup(cidrGroup));
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java
index 6f8035ae4e..1adc15df95 100644
--- a/src/java/org/apache/cassandra/tools/NodeProbe.java
+++ b/src/java/org/apache/cassandra/tools/NodeProbe.java
@@ -59,9 +59,14 @@ import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.audit.AuditLogManagerMBean;
import org.apache.cassandra.audit.AuditLogOptions;
import org.apache.cassandra.audit.AuditLogOptionsCompositeData;
+
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.auth.AuthCache;
import org.apache.cassandra.auth.AuthCacheMBean;
+import org.apache.cassandra.auth.CIDRGroupsMappingManager;
+import org.apache.cassandra.auth.CIDRGroupsMappingManagerMBean;
+import org.apache.cassandra.auth.CIDRPermissionsManager;
+import org.apache.cassandra.auth.CIDRPermissionsManagerMBean;
import org.apache.cassandra.auth.NetworkPermissionsCache;
import org.apache.cassandra.auth.NetworkPermissionsCacheMBean;
import org.apache.cassandra.auth.PasswordAuthenticator;
@@ -75,6 +80,8 @@ import org.apache.cassandra.batchlog.BatchlogManagerMBean;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
+import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable;
+import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTableMBean;
import org.apache.cassandra.fql.FullQueryLoggerOptions;
import org.apache.cassandra.fql.FullQueryLoggerOptionsCompositeData;
import org.apache.cassandra.gms.FailureDetector;
@@ -85,6 +92,7 @@ import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.hints.HintsServiceMBean;
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
+import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.StorageMetrics;
import org.apache.cassandra.metrics.TableMetrics;
@@ -102,6 +110,7 @@ import org.apache.cassandra.service.StorageServiceMBean;
import org.apache.cassandra.streaming.StreamManagerMBean;
import org.apache.cassandra.streaming.StreamState;
import org.apache.cassandra.streaming.management.StreamStateCompositeData;
+import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
import com.codahale.metrics.JmxReporter;
import com.google.common.base.Function;
@@ -155,11 +164,15 @@ public class NodeProbe implements AutoCloseable
protected PasswordAuthenticator.CredentialsCacheMBean ccProxy;
protected AuthorizationProxy.JmxPermissionsCacheMBean jpcProxy;
protected NetworkPermissionsCacheMBean npcProxy;
+ protected CIDRPermissionsManagerMBean cpbProxy;
+ protected CIDRGroupsMappingManagerMBean cmbProxy;
protected PermissionsCacheMBean pcProxy;
protected RolesCacheMBean rcProxy;
protected Output output;
private boolean failed;
+ protected CIDRFilteringMetricsTableMBean cfmProxy;
+
/**
* Creates a NodeProbe using the specified JMX host, port, username, and password.
*
@@ -275,12 +288,24 @@ public class NodeProbe implements AutoCloseable
ccProxy = JMX.newMBeanProxy(mbeanServerConn, name, PasswordAuthenticator.CredentialsCacheMBean.class);
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + AuthorizationProxy.JmxPermissionsCacheMBean.CACHE_NAME);
jpcProxy = JMX.newMBeanProxy(mbeanServerConn, name, AuthorizationProxy.JmxPermissionsCacheMBean.class);
+
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + NetworkPermissionsCache.CACHE_NAME);
npcProxy = JMX.newMBeanProxy(mbeanServerConn, name, NetworkPermissionsCacheMBean.class);
+
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + PermissionsCache.CACHE_NAME);
pcProxy = JMX.newMBeanProxy(mbeanServerConn, name, PermissionsCacheMBean.class);
+
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + RolesCache.CACHE_NAME);
rcProxy = JMX.newMBeanProxy(mbeanServerConn, name, RolesCacheMBean.class);
+
+ name = new ObjectName(CIDRPermissionsManager.MBEAN_NAME);
+ cpbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRPermissionsManagerMBean.class);
+
+ name = new ObjectName(CIDRGroupsMappingManager.MBEAN_NAME);
+ cmbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRGroupsMappingManagerMBean.class);
+
+ name = new ObjectName(CIDRFilteringMetricsTable.MBEAN_NAME);
+ cfmProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRFilteringMetricsTableMBean.class);
}
catch (MalformedObjectNameException e)
{
@@ -592,6 +617,41 @@ public class NodeProbe implements AutoCloseable
npcProxy.invalidateNetworkPermissions(roleName);
}
+ public boolean invalidateCidrPermissionsCache(String roleName)
+ {
+ return cpbProxy.invalidateCidrPermissionsCache(roleName);
+ }
+
+ public void reloadCidrGroupsCache()
+ {
+ cmbProxy.loadCidrGroupsCache();
+ }
+
+ public Set<String> listAvailableCidrGroups()
+ {
+ return cmbProxy.getAvailableCidrGroups();
+ }
+
+ public Set<String> listCidrsOfCidrGroup(String cidrGroup)
+ {
+ return cmbProxy.getCidrsOfCidrGroupAsStrings(cidrGroup);
+ }
+
+ public void updateCidrGroup(String cidrGroupName, List<String> cidrs)
+ {
+ cmbProxy.updateCidrGroup(cidrGroupName, cidrs);
+ }
+
+ public void dropCidrGroup(String cidrGroupName)
+ {
+ cmbProxy.dropCidrGroup(cidrGroupName);
+ }
+
+ public Set<String> getCidrGroupsOfIp(String ipStr)
+ {
+ return cmbProxy.getCidrGroupsOfIP(ipStr);
+ }
+
public void invalidatePermissionsCache()
{
pcProxy.invalidate();
@@ -1981,6 +2041,57 @@ public class NodeProbe implements AutoCloseable
}
}
+ public Object getCidrFilteringMetric(String metricName)
+ {
+ try
+ {
+ switch(metricName)
+ {
+ case CIDRAuthorizerMetrics.CIDR_CHECKS_LATENCY:
+ return JMX.newMBeanProxy(mbeanServerConn,
+ new ObjectName("org.apache.cassandra.metrics:type=CIDRAuthorization,name="
+ + metricName),
+ CassandraMetricsRegistry.JmxTimerMBean.class).getMean();
+ case CIDRAuthorizerMetrics.CIDR_GROUPS_CACHE_RELOAD_COUNT:
+ return JMX.newMBeanProxy(
+ mbeanServerConn,
+ new ObjectName("org.apache.cassandra.metrics:type=CIDRGroupsMappingCache,name=" + metricName),
+ CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
+ case CIDRAuthorizerMetrics.CIDR_GROUPS_CACHE_RELOAD_LATENCY:
+ case CIDRAuthorizerMetrics.LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY:
+ return JMX.newMBeanProxy(
+ mbeanServerConn,
+ new ObjectName("org.apache.cassandra.metrics:type=CIDRGroupsMappingCache,name=" + metricName),
+ CassandraMetricsRegistry.JmxTimerMBean.class).getMean();
+ default:
+ if (metricName.contains(CIDRAuthorizerMetrics.CIDR_ACCESSES_REJECTED_COUNT_PREFIX) ||
+ metricName.contains(CIDRAuthorizerMetrics.CIDR_ACCESSES_ACCEPTED_COUNT_PREFIX))
+ {
+ return JMX.newMBeanProxy(
+ mbeanServerConn,
+ new ObjectName("org.apache.cassandra.metrics:type=mymetricname,name=" + metricName),
+ CassandraMetricsRegistry.JmxCounterMBean.class).getCount();
+ }
+
+ throw new RuntimeException("Unknown metric " + metricName);
+ }
+ }
+ catch (MalformedObjectNameException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Map<String, Long> getCountsMetricsFromVtable()
+ {
+ return cfmProxy.getCountsMetricsFromVtable();
+ }
+
+ public Map<String, List<Double>> getLatenciesMetricsFromVtable()
+ {
+ return cfmProxy.getLatenciesMetricsFromVtable();
+ }
+
/**
* Retrieve Proxy metrics
* @param metricName Exceptions, Load, TotalHints or TotalHintsInProgress.
@@ -2215,6 +2326,21 @@ public class NodeProbe implements AutoCloseable
{
return ssProxy.getDefaultKeyspaceReplicationFactor();
}
+
+ public void printSet(PrintStream out, String colName, Set<String> values)
+ {
+ if (values == null || values.isEmpty())
+ return;
+
+ TableBuilder table = new TableBuilder();
+
+ table.add(colName + ": ");
+
+ for (String value : values)
+ table.add(value);
+
+ table.printTo(out);
+ }
}
class ColumnFamilyStoreMBeanIterator implements Iterator<Map.Entry<String, ColumnFamilyStoreMBean>>
diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java
index 5500240477..9c4d7858fd 100644
--- a/src/java/org/apache/cassandra/tools/NodeTool.java
+++ b/src/java/org/apache/cassandra/tools/NodeTool.java
@@ -98,6 +98,7 @@ public class NodeTool
CassHelp.class,
CfHistograms.class,
CfStats.class,
+ CIDRFilteringStats.class,
Cleanup.class,
ClearSnapshot.class,
ClientStats.class,
@@ -118,6 +119,7 @@ public class NodeTool
DisableHintsForDC.class,
DisableOldProtocolVersions.class,
Drain.class,
+ DropCIDRGroup.class,
EnableAuditLog.class,
EnableAutoCompaction.class,
EnableBackup.class,
@@ -134,6 +136,7 @@ public class NodeTool
GetAuditLog.class,
GetAuthCacheConfig.class,
GetBatchlogReplayTrottle.class,
+ GetCIDRGroupsOfIP.class,
GetColumnIndexSize.class,
GetCompactionThreshold.class,
GetCompactionThroughput.class,
@@ -155,15 +158,18 @@ public class NodeTool
GossipInfo.class,
Import.class,
Info.class,
+ InvalidateCIDRPermissionsCache.class,
InvalidateCounterCache.class,
InvalidateCredentialsCache.class,
InvalidateJmxPermissionsCache.class,
+ ReloadCIDRGroupsCache.class,
InvalidateKeyCache.class,
InvalidateNetworkPermissionsCache.class,
InvalidatePermissionsCache.class,
InvalidateRolesCache.class,
InvalidateRowCache.class,
Join.class,
+ ListCIDRGroups.class,
ListPendingHints.class,
ListSnapshots.class,
Move.class,
@@ -224,6 +230,7 @@ public class NodeTool
TopPartitions.class,
TpStats.class,
TruncateHints.class,
+ UpdateCIDRGroup.class,
UpgradeSSTable.class,
Verify.class,
Version.class,
diff --git a/src/java/org/apache/cassandra/tools/nodetool/CIDRFilteringStats.java b/src/java/org/apache/cassandra/tools/nodetool/CIDRFilteringStats.java
new file mode 100644
index 0000000000..9b01dc7894
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/CIDRFilteringStats.java
@@ -0,0 +1,93 @@
+/*
+ * 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.tools.nodetool;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import io.airlift.airline.Command;
+import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable;
+import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
+
+/**
+ * Nodetool command to view stats related to CIDR filtering
+ */
+@Command(name = "cidrfilteringstats", description = "Print statistics on CIDR filtering")
+public class CIDRFilteringStats extends NodeToolCmd
+{
+ private void printCountsMetrics(NodeProbe probe, PrintStream out)
+ {
+ out.println(CIDRFilteringMetricsCountsTable.TABLE_NAME + ": ");
+ TableBuilder outputTable = new TableBuilder();
+ outputTable.add(CIDRFilteringMetricsCountsTable.NAME_COL,
+ CIDRFilteringMetricsCountsTable.VALUE_COL);
+
+ Map<String, Long> metricRows = probe.getCountsMetricsFromVtable();
+
+ for (Map.Entry<String, Long> row : metricRows.entrySet())
+ {
+ outputTable.add(row.getKey(), Long.toString(row.getValue()));
+ }
+
+ outputTable.printTo(out);
+ out.println();
+ }
+
+ private void printLatenciesMetrics(NodeProbe probe, PrintStream out)
+ {
+ out.println(CIDRFilteringMetricsLatenciesTable.TABLE_NAME + ": ");
+ TableBuilder outputTable = new TableBuilder();
+ outputTable.add(CIDRFilteringMetricsLatenciesTable.NAME_COL,
+ CIDRFilteringMetricsLatenciesTable.P50_COL,
+ CIDRFilteringMetricsLatenciesTable.P95_COL,
+ CIDRFilteringMetricsLatenciesTable.P99_COL,
+ CIDRFilteringMetricsLatenciesTable.P999_COL,
+ CIDRFilteringMetricsLatenciesTable.MAX_COL);
+
+ Map<String, List<Double>> metricRows = probe.getLatenciesMetricsFromVtable();
+
+ for (Map.Entry<String, List<Double>> row : metricRows.entrySet())
+ {
+ List<String> values = new ArrayList<>();
+ values.add(row.getKey());
+ for (Double col : row.getValue())
+ {
+ values.add(Double.toString(col));
+ }
+ outputTable.add(values);
+ }
+
+ outputTable.printTo(out);
+ out.println();
+ }
+
+ @Override
+ public void execute(NodeProbe probe)
+ {
+ PrintStream out = probe.output().out;
+
+ // Read rows from corresponding virtual tables and print
+ printCountsMetrics(probe, out);
+ printLatenciesMetrics(probe, out);
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/DropCIDRGroup.java b/src/java/org/apache/cassandra/tools/nodetool/DropCIDRGroup.java
new file mode 100644
index 0000000000..2b432ea9f7
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/DropCIDRGroup.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.tools.nodetool;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.airlift.airline.Arguments;
+import io.airlift.airline.Command;
+import org.apache.cassandra.auth.AuthKeyspace;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Nodetool command to drop a CIDR group and associated mapping from the table {@link AuthKeyspace#CIDR_GROUPS}
+ */
+@Command(name = "dropcidrgroup", description = "Drop an existing cidr group")
+public class DropCIDRGroup extends NodeToolCmd
+{
+ @Arguments(usage = "<cidrGroup>", description = "Requires a cidr group name")
+ private List<String> args = new ArrayList<>();
+
+ @Override
+ public void execute(NodeProbe probe)
+ {
+ checkArgument(args.size() == 1, "dropcidrgroup command requires a cidr group name");
+
+ String cidrGroupName = args.get(0);
+ probe.dropCidrGroup(cidrGroupName);
+
+ probe.output().out.println("Deleted CIDR group " + cidrGroupName);
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIP.java b/src/java/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIP.java
new file mode 100644
index 0000000000..85c5194ed9
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIP.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.tools.nodetool;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.airlift.airline.Arguments;
+import io.airlift.airline.Command;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Nodetool command to get CIDR groups(s) of given IP
+ */
+@Command(name = "getcidrgroupsofip", description = "Print CIDR groups associated with given IP")
+public class GetCIDRGroupsOfIP extends NodeToolCmd
+{
+ @Arguments(usage = "<IP address>", description = "Requires IP address as a string")
+ private List<String> args = new ArrayList<>();
+
+ @Override
+ public void execute(NodeProbe probe)
+ {
+ PrintStream out = probe.output().out;
+
+ checkArgument(args.size() == 1, "Requires IP address as input");
+
+ String ipStr = args.get(0);
+ probe.printSet(out, "CIDR Groups", probe.getCidrGroupsOfIp(ipStr));
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCache.java b/src/java/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCache.java
new file mode 100644
index 0000000000..622bed3519
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCache.java
@@ -0,0 +1,56 @@
+/*
+ * 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.tools.nodetool;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.airlift.airline.Arguments;
+import io.airlift.airline.Command;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+/**
+ * Nodetool command to invalidate CIDR permissions cache, for a give role or for all roles in the cache.
+ */
+@Command(name = "invalidatecidrpermissionscache", description = "Invalidate the cidr permissions cache")
+public class InvalidateCIDRPermissionsCache extends NodeToolCmd
+{
+ @Arguments(usage = "[<role>...]", description = "List of roles to invalidate. By default, all roles")
+ private List<String> args = new ArrayList<>();
+
+ @Override
+ public void execute(NodeProbe probe)
+ {
+ if (args.isEmpty())
+ {
+ probe.invalidateCidrPermissionsCache("");
+ probe.output().out.println("Invalidated CIDR permissions cache");
+ }
+ else
+ {
+ for (String roleName : args)
+ {
+ if (probe.invalidateCidrPermissionsCache(roleName))
+ probe.output().out.println("Invalidated the role " + roleName + " from CIDR permissions cache");
+ else
+ probe.output().out.println("Not found role " + roleName + " in CIDR permissions cache, nothing to invalidate");
+ }
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java b/src/java/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCache.java
similarity index 51%
copy from test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
copy to src/java/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCache.java
index 51c61bbb12..0cf44e7cf8 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
+++ b/src/java/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCache.java
@@ -15,33 +15,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.cassandra.tools.nodetool;
-package org.apache.cassandra.cql3.statements;
+import io.airlift.airline.Command;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
-import org.junit.Assert;
-import org.junit.Test;
-
-import org.apache.cassandra.auth.DCPermissions;
-import org.apache.cassandra.cql3.CQLStatement;
-import org.apache.cassandra.cql3.QueryProcessor;
-
-public class CreateUserStatementTest
+/**
+ * Nodetool command to reload CIDR groups cache
+ */
+@Command(name = "reloadcidrgroupscache", description = "Reload CIDR groups cache with latest entries in cidr_groups table, when CIDR authorizer is enabled")
+public class ReloadCIDRGroupsCache extends NodeToolCmd
{
- private static CreateRoleStatement parse(String query)
- {
- CQLStatement.Raw stmt = QueryProcessor.parseStatement(query);
- Assert.assertTrue(stmt instanceof CreateRoleStatement);
- return (CreateRoleStatement) stmt;
- }
-
- private static DCPermissions dcPerms(String query)
- {
- return parse(query).dcPermissions;
- }
-
- @Test
- public void allDcsImplicit() throws Exception
+ @Override
+ public void execute(NodeProbe probe)
{
- Assert.assertFalse(dcPerms("CREATE USER u1").restrictsAccess());
+ probe.reloadCidrGroupsCache();
+ probe.output().out.println("Reloaded CIDR groups cache");
}
}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/UpdateCIDRGroup.java b/src/java/org/apache/cassandra/tools/nodetool/UpdateCIDRGroup.java
new file mode 100644
index 0000000000..36bef4ed8c
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/nodetool/UpdateCIDRGroup.java
@@ -0,0 +1,57 @@
+/*
+ * 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.tools.nodetool;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.airlift.airline.Arguments;
+import io.airlift.airline.Command;
+import org.apache.cassandra.auth.AuthKeyspace;
+import org.apache.cassandra.tools.NodeProbe;
+import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Nodetool command to insert/update a CIDR group and associated mapping in the table {@link AuthKeyspace#CIDR_GROUPS}
+ */
+@Command(name = "updatecidrgroup", description = "Insert/Update a cidr group")
+public class UpdateCIDRGroup extends NodeToolCmd
+{
+ @Arguments(usage = "[<cidrGroup> <cidr> ...]", description = "Requires a cidr group name, followed by one or more CIDRs separated by space")
+ private List<String> args = new ArrayList<>();
+
+ @Override
+ public void execute(NodeProbe probe)
+ {
+ checkArgument(args.size() > 1, "updatecidrgroup command requires a cidr group name and atleast one CIDR");
+
+ String cidrGroupName = args.get(0);
+
+ try
+ {
+ probe.updateCidrGroup(cidrGroupName, new ArrayList<>(args.subList(1, args.size())));
+ }
+ catch (IllegalArgumentException e)
+ {
+ // IllegalArgumentException message going to stdout, so throw different type of exception to land in stderr
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java
index ece6052a75..e7c9776718 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java
@@ -49,7 +49,8 @@ public class JMXGetterCheckTest extends TestBaseImpl
"org.apache.cassandra.db:type=StorageService:stopDaemon", // halts the instance, which then causes the JVM to exit
"org.apache.cassandra.db:type=StorageService:drain", // don't drain, it stops things which can cause other APIs to be unstable as we are in a stopped state
"org.apache.cassandra.db:type=StorageService:stopGossiping", // if we stop gossip this can cause other issues, so avoid
- "org.apache.cassandra.db:type=StorageService:resetLocalSchema" // this will fail when there are no other nodes which can serve schema
+ "org.apache.cassandra.db:type=StorageService:resetLocalSchema", // this will fail when there are no other nodes which can serve schema
+ "org.apache.cassandra.db:type=CIDRGroupsMappingManager:loadCidrGroupsCache" // AllowAllCIDRAuthorizer doesn't support this operation, as feature is disabled by default
);
public static final String JMX_SERVICE_URL_FMT = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi";
diff --git a/test/microbench/org/apache/cassandra/test/microbench/CIDRAuthorizerBench.java b/test/microbench/org/apache/cassandra/test/microbench/CIDRAuthorizerBench.java
new file mode 100644
index 0000000000..57ab736326
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/CIDRAuthorizerBench.java
@@ -0,0 +1,156 @@
+/*
+ * 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.test.microbench;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Benchmark CIDR authorizer (enforce mode)
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 200, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1, jvmArgsAppend = { "-Xmx512M", "-Djmh.executor=CUSTOM", "-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor" })
+@Threads(4) // make sure this matches the number of _physical_cores_
+@State(Scope.Benchmark)
+public class CIDRAuthorizerBench extends CQLTester
+{
+ static Random random = new Random();
+
+ static Map<String, List<String>> usersList = new HashMap<>();
+ static List<InetAddress> ipList = new ArrayList<>();
+ static Map<String, List<CIDR>> cidrsMapping = new HashMap<>();
+
+ @Setup(Level.Trial)
+ public void setup() throws IOException
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+
+ byte[] ipParts = new byte[4];
+ int numEntries = 100;
+
+ while (numEntries > 0)
+ {
+ ipParts[0] = (byte) random.nextInt(256);
+ ipParts[1] = (byte) random.nextInt(256);
+ ipParts[2] = (byte) random.nextInt(256);
+ InetAddress ip = InetAddress.getByAddress(ipParts);
+ ipList.add(ip);
+
+ String cidrGroupName = "cidrGroup" + numEntries;
+ cidrsMapping.put(cidrGroupName, Collections.singletonList(new CIDR(ip, (short)24)));
+
+ String userName = "user" + numEntries;
+ usersList.put(userName, Collections.singletonList(cidrGroupName));
+ numEntries--;
+ }
+
+ AuthTestUtils.createUsersWithCidrAccess(usersList);
+ AuthTestUtils.insertCidrsMappings(cidrsMapping);
+ }
+
+ @TearDown(Level.Trial)
+ public void teardown() throws IOException, ExecutionException, InterruptedException
+ {
+ CQLTester.cleanup();
+ }
+
+ @State(Scope.Thread)
+ public static class StateForValidLogin
+ {
+ InetSocketAddress ipAddr;
+ AuthenticatedUser user;
+
+ @Setup
+ public void setupStateForValidLogin() throws UnknownHostException
+ {
+ int randIndex = random.nextInt(usersList.size());
+
+ ipAddr = new InetSocketAddress(ipList.get(randIndex), 0);
+ user = new AuthenticatedUser("user" + randIndex);
+ }
+ }
+
+ @State(Scope.Thread)
+ public static class StateForInvalidLogin
+ {
+ InetSocketAddress ipAddr;
+ AuthenticatedUser user;
+
+ @Setup
+ public void setupStateForInvalidLogin() throws UnknownHostException
+ {
+ int randIndex = random.nextInt(usersList.size());
+
+ ipAddr = new InetSocketAddress(ipList.get(randIndex), 0);
+ user = new AuthenticatedUser("user" + (randIndex == 0 ? randIndex + 1 : randIndex - 1));
+ }
+ }
+
+ @Benchmark
+ public void benchCidrAuthorizer_ValidLogin(StateForValidLogin state)
+ {
+ state.user.hasAccessFromIp(state.ipAddr);
+ }
+
+ @Benchmark
+ public void benchCidrAuthorizer_InvalidLogin(StateForInvalidLogin state)
+ {
+ try
+ {
+ state.user.hasAccessFromIp(state.ipAddr);
+ }
+ catch (UnauthorizedException e)
+ {
+ // Exception expected for invalid login, do nothing here
+ }
+ }
+}
diff --git a/test/microbench/org/apache/cassandra/test/microbench/LongestMatchingCIDRBench.java b/test/microbench/org/apache/cassandra/test/microbench/LongestMatchingCIDRBench.java
new file mode 100644
index 0000000000..2cb555ed78
--- /dev/null
+++ b/test/microbench/org/apache/cassandra/test/microbench/LongestMatchingCIDRBench.java
@@ -0,0 +1,141 @@
+/*
+ * 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.test.microbench;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.auth.CIDRGroupsMappingTable;
+import org.apache.cassandra.cql3.CIDR;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Benchmark algorithm(s) of the Longest matching CIDR for given IP
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 30, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1, jvmArgsAppend = { "-Xmx512M", "-Djmh.executor=CUSTOM", "-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor" })
+@Threads(4) // make sure this matches the number of _physical_cores_
+@State(Scope.Benchmark)
+public class LongestMatchingCIDRBench
+{
+ CIDRGroupsMappingTable<String> ipv4CidrGroupsMappingTable;
+ static Random random = new Random();
+
+ static List<String> cidrsToInsert = Arrays.asList("100.90.80.70/14",
+ "110.100.90.80/15",
+ "120.110.100.90/15",
+ "130.120.110.100/15",
+ "140.130.120.110/15",
+ "150.140.130.120/16",
+ "160.150.140.130/16",
+ "170.160.150.140/16",
+ "180.170.160.150/16",
+ "190.180.170.160/16",
+ "200.190.180.170/16",
+ "210.200.190.180/16",
+ "220.210.200.190/17",
+ "230.220.210.200/17",
+ "240.230.220.210/17",
+ "250.240.230.220/18",
+ "255.250.240.230/18");
+
+ static List<String> ipsNotInserted = Arrays.asList("15.15.15.15",
+ "16.16.16.16",
+ "255.255.255.255",
+ "200.0.0.0",
+ "18.18.18.18",
+ "19.19.19.19",
+ "20.20.20.20",
+ "21.21.21.21",
+ "22.22.22.22",
+ "23.23.23.23",
+ "24.24.24.24",
+ "25.25.25.25",
+ "26.26.26.26",
+ "27.27.27.27",
+ "28.28.28.28",
+ "29.29.29.29",
+ "30.30.30.30");
+
+ @Setup(Level.Trial)
+ public void setup()
+ {
+ CIDRGroupsMappingTable.Builder<String> ipv4CidrGroupsMappingTableBuilder = CIDRGroupsMappingTable.builder(false);
+
+ for (String cidr : cidrsToInsert)
+ {
+ ipv4CidrGroupsMappingTableBuilder.add(CIDR.getInstance(cidr), cidr);
+ }
+ ipv4CidrGroupsMappingTable = ipv4CidrGroupsMappingTableBuilder.build();
+ }
+
+ @State(Scope.Thread)
+ public static class StateWithExistingCIDR
+ {
+ InetAddress ipAddr;
+
+ @Setup
+ public void setupStateWithExistingCidr() throws UnknownHostException
+ {
+ ipAddr = InetAddress.getByName(cidrsToInsert.get(random.nextInt(cidrsToInsert.size())).split("/")[0]);
+ }
+ }
+
+ @State(Scope.Thread)
+ public static class StateWithNonExistingCIDR
+ {
+ InetAddress ipAddr;
+
+ @Setup
+ public void setupStateWithNonExistingCidr() throws UnknownHostException
+ {
+ ipAddr = InetAddress.getByName(ipsNotInserted.get(random.nextInt(ipsNotInserted.size())));
+ }
+ }
+
+ @Benchmark
+ public void benchLongestMatchingCidr_CidrExistsCase(StateWithExistingCIDR state)
+ {
+ ipv4CidrGroupsMappingTable.lookupLongestMatchForIP(state.ipAddr);
+ }
+
+ @Benchmark
+ public void benchLongestMatchingCidr_CidrNotExistsCase(StateWithNonExistingCIDR state)
+ {
+ ipv4CidrGroupsMappingTable.lookupLongestMatchForIP(state.ipAddr);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/SchemaLoader.java b/test/unit/org/apache/cassandra/SchemaLoader.java
index f0178d1a2e..f341fb6eb8 100644
--- a/test/unit/org/apache/cassandra/SchemaLoader.java
+++ b/test/unit/org/apache/cassandra/SchemaLoader.java
@@ -29,6 +29,7 @@ import org.apache.cassandra.auth.AuthKeyspace;
import org.apache.cassandra.auth.AuthSchemaChangeListener;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IAuthorizer;
+import org.apache.cassandra.auth.ICIDRAuthorizer;
import org.apache.cassandra.auth.INetworkAuthorizer;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.config.*;
@@ -280,18 +281,21 @@ public class SchemaLoader
SchemaTestUtil.announceNewKeyspace(KeyspaceMetadata.create(name, params, tables, Views.none(), types, UserFunctions.none()));
}
- public static void setupAuth(IRoleManager roleManager, IAuthenticator authenticator, IAuthorizer authorizer, INetworkAuthorizer networkAuthorizer)
+ public static void setupAuth(IRoleManager roleManager, IAuthenticator authenticator, IAuthorizer authorizer, INetworkAuthorizer networkAuthorizer,
+ ICIDRAuthorizer cidrAuthorizer)
{
DatabaseDescriptor.setRoleManager(roleManager);
DatabaseDescriptor.setAuthenticator(authenticator);
DatabaseDescriptor.setAuthorizer(authorizer);
DatabaseDescriptor.setNetworkAuthorizer(networkAuthorizer);
+ DatabaseDescriptor.setCIDRAuthorizer(cidrAuthorizer);
SchemaTestUtil.announceNewKeyspace(AuthKeyspace.metadata());
DatabaseDescriptor.getRoleManager().setup();
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getInternodeAuthenticator().setupInternode();
DatabaseDescriptor.getAuthorizer().setup();
DatabaseDescriptor.getNetworkAuthorizer().setup();
+ DatabaseDescriptor.getCIDRAuthorizer().setup();
Schema.instance.registerListener(new AuthSchemaChangeListener());
}
diff --git a/test/unit/org/apache/cassandra/auth/AllowAllCIDRAuthorizerTest.java b/test/unit/org/apache/cassandra/auth/AllowAllCIDRAuthorizerTest.java
new file mode 100644
index 0000000000..b5c0e876ee
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/AllowAllCIDRAuthorizerTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.auth;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.service.ClientState;
+
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_PERMISSIONS;
+import static org.apache.cassandra.auth.AuthTestUtils.auth;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+
+/**
+ * Tests to verify AllowAllCIDRAuthorizer allows access from any CIDR
+ */
+public class AllowAllCIDRAuthorizerTest extends CQLTester
+{
+ private static void setupSuperUser()
+ {
+ QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) "
+ + "VALUES ('%s', true, true, '%s')",
+ AUTH_KEYSPACE_NAME,
+ AuthKeyspace.ROLES,
+ CassandraRoleManager.DEFAULT_SUPERUSER_NAME,
+ "xxx"));
+ }
+
+ @BeforeClass
+ public static void defineSchema() throws ConfigurationException
+ {
+ SchemaLoader.prepareServer();
+
+ SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
+ new AuthTestUtils.LocalPasswordAuthenticator(),
+ new AuthTestUtils.LocalCassandraAuthorizer(),
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ new AuthTestUtils.LocalAllowAllCIDRAuthorizer());
+ AuthCacheService.initializeAndRegisterCaches();
+ setupSuperUser();
+ }
+
+ @Before
+ public void before()
+ {
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_GROUPS).truncateBlocking();
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_PERMISSIONS).truncateBlocking();
+ }
+
+ private void testAccessSucceeds(String userName, String ip)
+ {
+ InetSocketAddress ipAddr = new InetSocketAddress(ip, 0);
+
+ AuthenticatedUser user = new AuthenticatedUser(userName);
+ Assert.assertTrue(user.hasAccessFromIp(ipAddr));
+
+ ClientState clientState = ClientState.forExternalCalls(ipAddr);
+ clientState.login(user);
+ clientState.validateLogin(); // expect no exception
+ }
+
+ @Test
+ public void testSuperUser()
+ {
+ testAccessSucceeds(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "10.20.30.5");
+ }
+
+ @Test
+ public void testUserWithoutCidrRestrictions()
+ {
+ String roleName = "role1";
+ auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true ",
+ roleName);
+ testAccessSucceeds(roleName, "127.0.0.1");
+ }
+
+ @Test
+ public void testUserWithRestrictingCidrGroups()
+ {
+ String roleName = "role1";
+ auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true AND ACCESS FROM CIDRS { 'cidrGroup1' }",
+ roleName);
+ testAccessSucceeds(roleName, "12.0.0.1");
+ }
+
+ @Test
+ public void testUserAllowedforAllCidrs()
+ {
+ String roleName = "role1";
+ auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true AND ACCESS FROM ALL CIDRS",
+ roleName);
+ testAccessSucceeds(roleName, "17.0.0.1");
+ }
+
+ private UntypedResultSet getCidrGroups(String role)
+ {
+ String query = String.format("SELECT %s FROM %s.%s WHERE role = '%s'",
+ "cidr_groups",
+ AUTH_KEYSPACE_NAME,
+ CIDR_PERMISSIONS,
+ RoleResource.role(role).getRoleName());
+ return QueryProcessor.executeInternal(query);
+ }
+
+ // Create role command with access from cidrs clause should work even when CIDR authorizer is disabled
+ @Test
+ public void testCreateAndDropRoleWithCidrsClause() throws Throwable
+ {
+ String role = "role1";
+
+ assertEmpty(getCidrGroups(role));
+
+ AuthTestUtils.createUsersWithCidrAccess(Collections.singletonMap(role,
+ Collections.singletonList("cidrGroup1")));
+ UntypedResultSet results = getCidrGroups(role);
+ Assert.assertEquals(Sets.newHashSet("cidrGroup1"),
+ Iterables.getOnlyElement(results).getFrozenSet("cidr_groups", UTF8Type.instance));
+
+ AuthTestUtils.auth("DROP ROLE %s", role);
+ assertEmpty(getCidrGroups(role));
+ }
+
+ // Alter role command with access from cidrs clause should work even when CIDR authorizer is disabled
+ @Test
+ public void testAlterAndDropRoleWithCidrsClause() throws Throwable
+ {
+ String role = "role1";
+
+ assertEmpty(getCidrGroups(role));
+
+ auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true ", role);
+ auth("ALTER ROLE %s WITH ACCESS FROM CIDRS {'%s'}", role, "cidrGroup1");
+
+ UntypedResultSet results = getCidrGroups(role);
+ Assert.assertEquals(Sets.newHashSet("cidrGroup1"),
+ Iterables.getOnlyElement(results).getFrozenSet("cidr_groups", UTF8Type.instance));
+
+ AuthTestUtils.auth("DROP ROLE %s", role);
+ assertEmpty(getCidrGroups(role));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/AuthTestUtils.java b/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
index e3ef505be9..610832ffb4 100644
--- a/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
+++ b/test/unit/org/apache/cassandra/auth/AuthTestUtils.java
@@ -18,6 +18,8 @@
package org.apache.cassandra.auth;
+import java.util.List;
+import java.util.Map;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
@@ -32,20 +34,31 @@ import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.cql3.statements.AlterRoleStatement;
+import org.apache.cassandra.cql3.statements.AuthenticationStatement;
import org.apache.cassandra.cql3.statements.BatchStatement;
+import org.apache.cassandra.cql3.statements.CreateRoleStatement;
+import org.apache.cassandra.cql3.statements.DropRoleStatement;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.transport.messages.ResultMessage;
+import static java.lang.String.format;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
import static org.junit.Assert.assertNotNull;
@@ -133,6 +146,84 @@ public class AuthTestUtils
}
}
+ public static class LocalCIDRGroupsMappingManager extends CIDRGroupsMappingManager
+ {
+ @Override
+ ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
+ {
+ return statement.executeLocally(QueryState.forInternalCalls(), options);
+ }
+
+ @Override
+ UntypedResultSet process(String query, ConsistencyLevel cl)
+ {
+ return QueryProcessor.executeInternal(query);
+ }
+
+ public static String getCidrTuplesSet(List<CIDR> cidrs)
+ {
+ return getCidrTuplesSetString(cidrs);
+ }
+ }
+
+ public static class LocalCIDRPermissionsManager extends CIDRPermissionsManager
+ {
+ @Override
+ public ResultMessage.Rows select(SelectStatement statement, QueryOptions options)
+ {
+ return statement.executeLocally(QueryState.forInternalCalls(), options);
+ }
+
+ @Override
+ public UntypedResultSet process(String query, ConsistencyLevel cl)
+ {
+ return QueryProcessor.executeInternal(query);
+ }
+ }
+
+ public static class LocalCassandraCIDRAuthorizer extends CassandraCIDRAuthorizer
+ {
+ CIDRAuthorizerMode cidrAuthorizerMode;
+
+ public LocalCassandraCIDRAuthorizer()
+ {
+ cidrAuthorizerMode = CIDRAuthorizerMode.ENFORCE;
+ }
+
+ public LocalCassandraCIDRAuthorizer(CIDRAuthorizerMode mode)
+ {
+ cidrAuthorizerMode = mode;
+ }
+
+ @Override
+ protected void createManagers()
+ {
+ cidrPermissionsManager = new LocalCIDRPermissionsManager();
+ cidrGroupsMappingManager = new LocalCIDRGroupsMappingManager();
+ }
+
+ @Override
+ protected boolean isMonitorMode()
+ {
+ return cidrAuthorizerMode == CIDRAuthorizerMode.MONITOR;
+ }
+
+ CIDRPermissionsCache getCidrPermissionsCache()
+ {
+ return cidrPermissionsCache;
+ }
+ }
+
+ public static class LocalAllowAllCIDRAuthorizer extends AllowAllCIDRAuthorizer
+ {
+ @Override
+ protected void createManagers()
+ {
+ cidrPermissionsManager = new LocalCIDRPermissionsManager();
+ cidrGroupsMappingManager = new LocalCIDRGroupsMappingManager();
+ }
+ }
+
public static class LocalPasswordAuthenticator extends PasswordAuthenticator
{
@Override
@@ -170,6 +261,13 @@ public class AuthTestUtils
return networkPemissionsTable.metric.readLatency.latency.getCount();
}
+ public static long getCidrPermissionsReadCount()
+ {
+ ColumnFamilyStore cidrPemissionsTable =
+ Keyspace.open(SchemaConstants.AUTH_KEYSPACE_NAME).getColumnFamilyStore(AuthKeyspace.CIDR_PERMISSIONS);
+ return cidrPemissionsTable.metric.readLatency.latency.getCount();
+ }
+
public static long getRolePermissionsReadCount()
{
ColumnFamilyStore rolesPemissionsTable =
@@ -192,6 +290,59 @@ public class AuthTestUtils
return roleOptions;
}
+ private static ClientState getClientState()
+ {
+ ClientState state = ClientState.forInternalCalls();
+ state.login(new AuthenticatedUser(CassandraRoleManager.DEFAULT_SUPERUSER_NAME));
+ return state;
+ }
+
+ public static AuthenticationStatement authWithoutInvalidate(String query, Object... args)
+ {
+ CQLStatement statement = QueryProcessor.parseStatement(String.format(query, args)).prepare(ClientState.forInternalCalls());
+ assert statement instanceof CreateRoleStatement
+ || statement instanceof AlterRoleStatement
+ || statement instanceof DropRoleStatement;
+ AuthenticationStatement authStmt = (AuthenticationStatement) statement;
+
+ authStmt.execute(getClientState());
+
+ return authStmt;
+ }
+
+ public static AuthenticationStatement auth(String query, Object... args)
+ {
+ AuthenticationStatement authStmt = authWithoutInvalidate(query, args);
+
+ // invalidate roles cache so that any changes to the underlying roles are picked up
+ Roles.cache.invalidate();
+
+ return authStmt;
+ }
+
+ public static void createUsersWithCidrAccess(Map<String, List<String>> userToCidrPermsMapping)
+ {
+ for (Map.Entry<String, List<String>> userMapping : userToCidrPermsMapping.entrySet())
+ {
+ authWithoutInvalidate(
+ "CREATE ROLE %s WITH password = 'password' AND LOGIN = true AND ACCESS FROM CIDRS {'%s'}",
+ userMapping.getKey(), String.join("', '", userMapping.getValue()));
+ }
+ Roles.cache.invalidate();
+ }
+
+ public static void insertCidrsMappings(Map<String, List<CIDR>> cidrsMapping)
+ {
+ for (Map.Entry<String, List<CIDR>> cidrMapping : cidrsMapping.entrySet())
+ {
+ QueryProcessor.executeInternal(format("insert into %s.%s(cidr_group, cidrs) values('%s', %s );",
+ AUTH_KEYSPACE_NAME, CIDR_GROUPS, cidrMapping.getKey(),
+ AuthTestUtils.LocalCIDRGroupsMappingManager.getCidrTuplesSet(
+ cidrMapping.getValue())));
+ }
+ DatabaseDescriptor.getCIDRAuthorizer().loadCidrGroupsCache(); // update cache with CIDRs inserted above
+ }
+
// mTLS authenticators related utility methods
public static InetAddress getMockInetAddress() throws UnknownHostException
{
diff --git a/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingIntervalTreeTest.java b/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingIntervalTreeTest.java
new file mode 100644
index 0000000000..699aa2cd2c
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingIntervalTreeTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.Test;
+
+import org.apache.cassandra.auth.CIDRGroupsMappingIntervalTree.IPIntervalNode;
+import org.apache.cassandra.cql3.CIDR;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test the internal data structure used for the interval tree.
+ * The test cases for CIDRGroupsMappingTable functionality can be found at {@link CIDRGroupsMappingTableTest}
+ */
+public class CIDRGroupsMappingIntervalTreeTest
+{
+ private IPIntervalNode<Void> createInternalNode(String parentCidr, IPIntervalNode<Void>[] children)
+ {
+ return new IPIntervalNode<>(CIDR.getInstance(parentCidr), null, children);
+ }
+
+ @SuppressWarnings("unchecked")
+ private IPIntervalNode<Void>[] createLeaves(List<String> cidrs)
+ {
+ return cidrs.stream().map(this::createLeafNode).toArray(IPIntervalNode[]::new);
+ }
+
+ private IPIntervalNode<Void> createLeafNode(String cidr)
+ {
+ return new IPIntervalNode<>(CIDR.getInstance(cidr), null, null);
+ }
+
+ private List<CIDR> toCidrs(IPIntervalNode<Void>[] nodes)
+ {
+ return Arrays.stream(nodes).map(IPIntervalNode::cidr).collect(Collectors.toList());
+ }
+
+ @Test
+ public void testInternalNodeAllLeft()
+ {
+ List<String> cidrs = Arrays.asList("10.0.0.1/24",
+ "10.0.1.1/24",
+ "10.0.2.1/24",
+ "10.0.3.1/24");
+ String parentCidr = "11.0.0.1/30";
+
+ IPIntervalNode<Void>[] children = createLeaves(cidrs);
+ IPIntervalNode<Void> internalNode = createInternalNode(parentCidr, children);
+
+ assertThat(internalNode.left()).isEqualTo(children);
+ assertThat(internalNode.right()).isNull();
+ }
+
+ @Test
+ public void testInternalNodeAllRight()
+ {
+ List<String> cidrs = Arrays.asList("10.0.0.1/24",
+ "10.0.1.1/24",
+ "10.0.2.1/24",
+ "10.0.3.1/24");
+ String parentCidr = "9.0.0.1/30";
+
+ IPIntervalNode<Void>[] children = createLeaves(cidrs);
+ IPIntervalNode<Void> internalNode = createInternalNode(parentCidr, children);
+
+ assertThat(internalNode.left()).isNull();
+ assertThat(internalNode.right()).isEqualTo(children);
+ }
+
+ @Test
+ public void testInternalNodeNoOverlapWithParent() // parent node does not overlap with closest
+ {
+ List<String> cidrs = Arrays.asList("10.0.0.1/24",
+ "10.0.1.1/24",
+ "10.0.3.1/24",
+ "10.0.4.1/24");
+ String parentCidr = "10.0.2.1/30";
+
+ IPIntervalNode<Void>[] children = createLeaves(cidrs);
+ IPIntervalNode<Void> internalNode = new IPIntervalNode<>(CIDR.getInstance(parentCidr), null, children);
+
+ assertThat(toCidrs(internalNode.left())).describedAs("Check left nodes")
+ .isEqualTo(toCidrs(createLeaves(Arrays.asList("10.0.0.1/24",
+ "10.0.1.1/24"))));
+
+ assertThat(toCidrs(internalNode.right())).describedAs("Check right nodes")
+ .isEqualTo(toCidrs(createLeaves(Arrays.asList("10.0.3.1/24",
+ "10.0.4.1/24"))));
+ }
+
+ @Test
+ public void testInternalNodeNoOverlapWithParentIpv6() // parent node does not overlap with closest
+ {
+ List<String> cidrs = Arrays.asList("aaaa::0001:ffff/112",
+ "bbbb::0001:ffff/112",
+ "dddd::0001:ffff/112",
+ "eeee::0001:ffff/112");
+ String parentCidr = "cccc::0001:ffff/113";
+
+ IPIntervalNode<Void>[] children = createLeaves(cidrs);
+ IPIntervalNode<Void> internalNode = new IPIntervalNode<>(CIDR.getInstance(parentCidr), null, children);
+
+ assertThat(toCidrs(internalNode.left())).describedAs("Check left nodes")
+ .isEqualTo(toCidrs(createLeaves(Arrays.asList("aaaa::0001:ffff/112",
+ "bbbb::0001:ffff/112"))));
+
+ assertThat(toCidrs(internalNode.right())).describedAs("Check right nodes")
+ .isEqualTo(toCidrs(createLeaves(
+ Arrays.asList("dddd::0001:ffff/112", "eeee::0001:ffff/112"))));
+ }
+
+ @Test
+ public void testInternalNodeOverlapWithParent() // parent node overlaps with closest
+ {
+ List<String> cidrs = Arrays.asList("10.0.0.1/24",
+ "10.0.1.1/24",
+ "10.0.2.1/24",
+ "10.0.3.1/24");
+ String parentCidr = "10.0.2.1/25"; // overlaps with "10.0.2.1/24"
+
+ IPIntervalNode<Void>[] children = createLeaves(cidrs);
+ IPIntervalNode<Void> internalNode = new IPIntervalNode<>(CIDR.getInstance(parentCidr), null, children);
+
+ // The overlapping node "10.0.2.1/24" should be included in both left and right
+ assertThat(toCidrs(internalNode.left())).describedAs("Check left nodes")
+ .isEqualTo(toCidrs(createLeaves(Arrays.asList("10.0.0.1/24",
+ "10.0.1.1/24",
+ "10.0.2.1/24"))));
+
+ assertThat(toCidrs(internalNode.right())).describedAs("Check right nodes")
+ .isEqualTo(toCidrs(createLeaves(Arrays.asList("10.0.2.1/24",
+ "10.0.3.1/24"))));
+ }
+
+ @Test
+ public void testInternalNodeOverlapWithParentIpv6() // parent node overlaps with closest - IPv6
+ {
+ List<String> cidrs = Arrays.asList("aaaa::0001:ffff/112",
+ "bbbb::0001:ffff/112",
+ "cccc::0001:ffff/112",
+ "dddd::0001:ffff/112",
+ "eeee::0001:ffff/112");
+ String parentCidr = "cccc::0001:ffff/113"; // overlaps with "cccc::0001:ffff/112"
+
+ IPIntervalNode<Void>[] children = createLeaves(cidrs);
+ IPIntervalNode<Void> internalNode = new IPIntervalNode<>(CIDR.getInstance(parentCidr), null, children);
+
+ // The overlapping node "10.0.2.1/24" should be included in both left and right
+ assertThat(toCidrs(internalNode.left())).describedAs("Check left nodes")
+ .isEqualTo(toCidrs(createLeaves(Arrays.asList("aaaa::0001:ffff/112",
+ "bbbb::0001:ffff/112",
+ "cccc::0001:ffff/112"))));
+
+ assertThat(toCidrs(internalNode.right())).describedAs("Check right nodes")
+ .isEqualTo(toCidrs(createLeaves(
+ Arrays.asList("cccc::0001:ffff/112",
+ "dddd::0001:ffff/112",
+ "eeee::0001:ffff/112"))));
+ }
+
+ @Test
+ public void testSearchReturnsExactMatch() throws UnknownHostException
+ {
+ List<String> cidrs = Arrays.asList("10.0.0.1/24",
+ "10.0.1.1/24",
+ "10.0.2.1/24",
+ "10.0.3.1/24");
+
+ IPIntervalNode<Void>[] nodes = createLeaves(cidrs);
+
+ // exhaust all IPs covered by the CIDRs list
+ for (int i = 0; i < cidrs.size(); i++)
+ {
+ for (int j = 1; j < 256; j++)
+ {
+ String ip = String.format("10.0.%d.%d", i, j);
+ int index = IPIntervalNode.binarySearchNodesIndex(nodes, InetAddress.getByName(ip));
+ assertThat(index).isEqualTo(i);
+ }
+ }
+ }
+
+ @Test
+ public void testSearchReturnsClosest() throws UnknownHostException
+ {
+ List<String> cidrs = Arrays.asList("10.0.0.1/24",
+ "10.0.2.1/24",
+ "10.0.4.1/24",
+ "10.0.6.1/24",
+ "10.0.8.1/24"
+ );
+
+ IPIntervalNode<Void>[] nodes = createLeaves(cidrs);
+
+ for (int i = 1, expected = 0; i < 10; i++)
+ {
+ if (i % 2 == 0) // skip the even values as they are in CIDRs
+ continue;
+
+ for (int j = 1; j < 256; j++)
+ {
+ String ip = String.format("10.0.%d.%d", i, j);
+ int index = IPIntervalNode.binarySearchNodesIndex(nodes, InetAddress.getByName(ip));
+ assertThat(index).describedAs("The closest node is left to the ip").isEqualTo(expected);
+ }
+ expected++;
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingManagerTest.java b/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingManagerTest.java
new file mode 100644
index 0000000000..a3a899d65d
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingManagerTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.auth;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+
+public class CIDRGroupsMappingManagerTest
+{
+ CIDRGroupsMappingManager cidrGroupsMappingManager;
+
+ private static void setupSuperUser()
+ {
+ QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) "
+ + "VALUES ('%s', true, true, '%s')",
+ AUTH_KEYSPACE_NAME,
+ AuthKeyspace.ROLES,
+ CassandraRoleManager.DEFAULT_SUPERUSER_NAME,
+ "xxx"));
+ }
+
+ @BeforeClass
+ public static void defineSchema() throws ConfigurationException
+ {
+ SchemaLoader.prepareServer();
+
+ SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
+ new AuthTestUtils.LocalPasswordAuthenticator(),
+ new AuthTestUtils.LocalCassandraAuthorizer(),
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ new AuthTestUtils.LocalCassandraCIDRAuthorizer());
+ AuthCacheService.initializeAndRegisterCaches();
+ setupSuperUser();
+ }
+
+ @Before
+ public void setup()
+ {
+ cidrGroupsMappingManager = new AuthTestUtils.LocalCIDRGroupsMappingManager();
+ cidrGroupsMappingManager.setup();
+
+ Map<String, List<CIDR>> cidrsMapping = new HashMap<String, List<CIDR>>()
+ {{
+ put("cidrGroup1", Collections.singletonList(CIDR.getInstance("10.20.30.5/24")));
+ put("cidrGroup2", Arrays.asList(CIDR.getInstance("20.30.40.6/16"), CIDR.getInstance("30.40.50.6/8")));
+ put("cidrGroup3", Arrays.asList(CIDR.getInstance("40.50.60.7/32"), CIDR.getInstance("50.60.70.80/10"), CIDR.getInstance("60.70.80.90/22")));
+ }};
+
+ AuthTestUtils.insertCidrsMappings(cidrsMapping);
+ }
+
+ @Test
+ public void testGetCidrsOfCidrGroupAsStrings()
+ {
+ Set<String> cidrs = cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup2");
+ assertEquals(ImmutableSet.of("20.30.0.0/16", "30.0.0.0/8"), cidrs);
+ }
+
+ @Test
+ public void testUpdateCidrGroup()
+ {
+ cidrGroupsMappingManager.updateCidrGroup("cidrGroup3",
+ Arrays.asList("140.50.60.7/32", "150.60.70.80/10"));
+ assertEquals(ImmutableSet.of("150.0.0.0/10", "140.50.60.7/32"),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup3"));
+ }
+
+ @Test
+ public void testDropCidrGroupForExistingCidrGroup()
+ {
+ cidrGroupsMappingManager.dropCidrGroup("cidrGroup1");
+ assertEquals(2, cidrGroupsMappingManager.getAvailableCidrGroups().size());
+ }
+
+ @Test
+ public void testDropCidrGroupForNonExistingCidrGroup()
+ {
+ assertThatThrownBy(() -> cidrGroupsMappingManager.dropCidrGroup("cidrGroup10"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessage("CIDR group 'cidrGroup10' doesn't exists");
+ }
+
+ @Test
+ public void testDropCidrGroupIfExistsForExistingCidrGroup()
+ {
+ cidrGroupsMappingManager.dropCidrGroupIfExists("cidrGroup1");
+ assertEquals(2, cidrGroupsMappingManager.getAvailableCidrGroups().size());
+ }
+
+ @Test
+ public void testDropCidrGroupIfExistsForNonExistingCidrGroup()
+ {
+ // expect no exception
+ cidrGroupsMappingManager.dropCidrGroupIfExists("cidrGroup10");
+ }
+
+ @Test
+ public void testRecreateCidrGroupsMapping()
+ {
+ assertEquals(3, cidrGroupsMappingManager.getAvailableCidrGroups().size());
+ assertEquals(ImmutableSet.of("10.20.30.0/24"),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup1"));
+ assertEquals(ImmutableSet.of("20.30.0.0/16", "30.0.0.0/8"),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup2"));
+ assertEquals(ImmutableSet.of("40.50.60.7/32", "50.0.0.0/10", "60.70.80.0/22"),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup3"));
+
+ // load table with new mappings
+ Map<String, List<String>> cidrsMapping = new HashMap<String, List<String>>()
+ {{
+ put("cidrGroup11", Collections.singletonList("100.20.30.5/24"));
+ put("cidrGroup12", Arrays.asList("120.30.40.6/16", "130.40.50.6/8"));
+ }};
+ cidrGroupsMappingManager.recreateCidrGroupsMapping(cidrsMapping);
+
+ assertEquals(2, cidrGroupsMappingManager.getAvailableCidrGroups().size());
+ assertEquals(ImmutableSet.of("100.20.30.0/24"),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup11"));
+ assertEquals(ImmutableSet.of("120.30.0.0/16", "130.0.0.0/8"),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup12"));
+
+ // ensure stale cidr groups are deleted
+ assertEquals(Collections.emptySet(),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup1"));
+ assertEquals(Collections.emptySet(),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup2"));
+ assertEquals(Collections.emptySet(),
+ cidrGroupsMappingManager.getCidrsOfCidrGroupAsStrings("cidrGroup3"));
+ }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingTableTest.java b/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingTableTest.java
new file mode 100644
index 0000000000..9353d96fba
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/CIDRGroupsMappingTableTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.auth;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import com.google.common.collect.Sets;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CIDR;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class CIDRGroupsMappingTableTest
+{
+ private final CIDRGroupsMappingTable.Builder<String> ipv4CidrGroupsMappingTableBuilder;
+ private final CIDRGroupsMappingTable.Builder<String> ipv6CidrGroupsMappingTableBuilder;
+
+ public CIDRGroupsMappingTableTest()
+ {
+ ipv4CidrGroupsMappingTableBuilder = CIDRGroupsMappingTable.builder(false);
+ ipv6CidrGroupsMappingTableBuilder = CIDRGroupsMappingTable.builder(true);
+ }
+
+ private CIDRGroupsMappingTable<String> buildCidrGroupsMappingTable(List<String> cidrs,
+ CIDRGroupsMappingTable.Builder<String> cidrGroupsMappingTableBuilder)
+ {
+ for (String cidr : cidrs)
+ {
+ cidrGroupsMappingTableBuilder.add(CIDR.getInstance(cidr), cidr);
+ }
+
+ return cidrGroupsMappingTableBuilder.build();
+ }
+
+ private Set<String> longestMatchForIP(CIDRGroupsMappingTable<String> cidrGroupsMappingTable, String ip)
+ {
+ try
+ {
+ InetAddress ipAddr = InetAddress.getByName(ip);
+ return cidrGroupsMappingTable.lookupLongestMatchForIP(ipAddr);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new IllegalArgumentException(String.format("%s is not a valid IP String", ip));
+ }
+ }
+
+ // expects one and single one result
+ private String singleMatchForIp(CIDRGroupsMappingTable<String> cidrGroupsMappingTable, String ip)
+ {
+ return longestMatchForIP(cidrGroupsMappingTable, ip).iterator().next();
+ }
+
+ private String ip(String cidr)
+ {
+ return cidr.split("/")[0];
+ }
+
+ @Test
+ public void insertIpv4CIDRs()
+ {
+ List<String> validCIDRs = Arrays.asList(
+ "128.10.120.2/10",
+ "128.20.120.2/20",
+ "0.0.0.0/0",
+ "10.1.1.2/10",
+ "255.0.0.0/8"
+ );
+
+ CIDRGroupsMappingTable<String> ipv4CidrGroupsMappingTable =
+ buildCidrGroupsMappingTable(validCIDRs, ipv4CidrGroupsMappingTableBuilder);
+
+ for (String cidr : validCIDRs)
+ {
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, ip(cidr))).isEqualTo(cidr);
+ }
+ }
+
+ private void testIpv6CIDRs()
+ {
+ List<String> validCIDRs = Arrays.asList("2001:0db8:0:0:0:0:1234:5678/127",
+ "2001:0db8:3333:4444:CCCC:DDDD:EEEE:FFFF/128",
+ "2001:0db8:3333:4444:5555:6666:7777:8800/120",
+ "2001:0db8::/30",
+ "::1234:5678/40",
+ "f000::/4",
+ "ffff::/16"
+ );
+
+ Collections.shuffle(validCIDRs);
+
+ CIDRGroupsMappingTable<String> ipv6CidrGroupsMappingTable =
+ buildCidrGroupsMappingTable(validCIDRs, ipv6CidrGroupsMappingTableBuilder);
+
+ for (String cidr : validCIDRs)
+ {
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, ip(cidr))).isEqualTo(cidr);
+ }
+
+ assertThat(longestMatchForIP(ipv6CidrGroupsMappingTable, "0:0:0100::")).isNull();
+ assertThat(longestMatchForIP(ipv6CidrGroupsMappingTable, "1001::5678")).isNull();
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, "ffff::5678")).isEqualTo("ffff::/16");
+
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, "2001:0db8:3333:4444:5555::"))
+ .isEqualTo("2001:0db8::/30");
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, "2001:0db8:3333:4444:CCCC::"))
+ .isEqualTo("2001:0db8::/30");
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, "2001:0db8:3333:4444:5555:6666:7777:8822"))
+ .isEqualTo("2001:0db8:3333:4444:5555:6666:7777:8800/120");
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, "2001:0db8:3333:4444:5555:6666:7777:8282"))
+ .isEqualTo("2001:0db8::/30");
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, "2001:0db8::1234:5679"))
+ .isEqualTo("2001:0db8:0:0:0:0:1234:5678/127");
+ assertThat(singleMatchForIp(ipv6CidrGroupsMappingTable, "2001:0db8::1234:5698"))
+ .isEqualTo("2001:0db8::/30");
+ }
+
+ @Test
+ public void testIpv6CIDRsInRandomOrder()
+ {
+ for (int i = 0; i < 20; i++)
+ testIpv6CIDRs();
+ }
+
+ @Test
+ public void ipv4MappedCIDRsInIpv6Tree()
+ {
+ // IPv4 mapped IPv6 gets converted to IPv4
+ ipv6CidrGroupsMappingTableBuilder.add(CIDR.getInstance("0:0:0:0:0:ffff:192.1.56.10/96"),
+ "0:0:0:0:0:ffff:192.1.56.10/96");
+ assertThatThrownBy(ipv6CidrGroupsMappingTableBuilder::build)
+ .hasMessage("Invalid CIDR format, expecting IPv6, received IPv4")
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void duplicateKeys()
+ {
+ List<String> validCIDRs = Arrays.asList("0:0:0:0:0:ffff:192.1.56.10/100", "::ffff:192.1.56.10/100");
+ CIDRGroupsMappingTable<String> ipv4CidrGroupsMappingTable =
+ buildCidrGroupsMappingTable(validCIDRs, ipv4CidrGroupsMappingTableBuilder);
+
+ Set<String> result = longestMatchForIP(ipv4CidrGroupsMappingTable, "0:0:0:0:0:ffff:192.1.56.10");
+ Set<String> expected = Sets.newHashSet(validCIDRs);
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ public void withOverlappingCIDRs()
+ {
+ List<String> validCIDRs = Arrays.asList("127.0.0.0/24", "127.0.1.0/24", "0.0.0.0/24");
+ CIDRGroupsMappingTable<String> ipv4CidrGroupsMappingTable =
+ buildCidrGroupsMappingTable(validCIDRs, ipv4CidrGroupsMappingTableBuilder);
+
+ assertThat(longestMatchForIP(ipv4CidrGroupsMappingTable, "127.1.1.5")).isNull();
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "127.0.0.5")).isEqualTo("127.0.0.0/24");
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "127.0.1.5")).isEqualTo("127.0.1.0/24");
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "0.0.0.5")).isEqualTo("0.0.0.0/24");
+ assertThat(longestMatchForIP(ipv4CidrGroupsMappingTable, "0.0.1.5")).isNull();
+ }
+
+ @Test
+ public void withNetMaskZero()
+ {
+ List<String> validCIDRs = Arrays.asList("0.0.0.0/0",
+ "127.0.0.0/24",
+ "127.0.0.112/26",
+ "112.0.0.0/6");
+ CIDRGroupsMappingTable<String> ipv4CidrGroupsMappingTable =
+ buildCidrGroupsMappingTable(validCIDRs, ipv4CidrGroupsMappingTableBuilder);
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "127.1.1.5")).isEqualTo("0.0.0.0/0");
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "127.0.0.114")).isEqualTo("127.0.0.112/26");
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "113.0.0.5")).isEqualTo("112.0.0.0/6");
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "127.0.0.5")).isEqualTo("127.0.0.0/24");
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "120.0.0.5")).isEqualTo("0.0.0.0/0");
+ assertThat(singleMatchForIp(ipv4CidrGroupsMappingTable, "127.0.0.64")).isEqualTo("127.0.0.112/26");
+ }
+
+ @Test
+ public void testLongestMatchWithInvalidIP()
+ {
+ CIDRGroupsMappingTable<String> ipv4CidrGroupsMappingTable =
+ buildCidrGroupsMappingTable(Collections.singletonList("20.20.20.20/20"), ipv4CidrGroupsMappingTableBuilder);
+ assertThatThrownBy(() -> longestMatchForIP(ipv4CidrGroupsMappingTable, "2000.20"))
+ .hasMessage("2000.20 is not a valid IP String")
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void testEmptyCidrsList()
+ {
+ CIDRGroupsMappingTable<String> ipv4CidrGroupsMappingTable =
+ buildCidrGroupsMappingTable(Collections.emptyList(), ipv4CidrGroupsMappingTableBuilder);
+ Set<String> cidrGroups = longestMatchForIP(ipv4CidrGroupsMappingTable, "20.20.20.20");
+ assertThat(cidrGroups).isEmpty();
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerEnforceModeTest.java b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerEnforceModeTest.java
new file mode 100644
index 0000000000..ce0ea16519
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerEnforceModeTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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.auth;
+
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.exceptions.UnauthorizedException;
+import org.apache.cassandra.service.ClientState;
+
+import static java.lang.String.format;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_PERMISSIONS;
+import static org.apache.cassandra.auth.AuthTestUtils.auth;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+public class CassandraCIDRAuthorizerEnforceModeTest extends CQLTester
+{
+ private static final AuthTestUtils.LocalCassandraCIDRAuthorizer cidrAuthorizer = new AuthTestUtils.LocalCassandraCIDRAuthorizer();
+
+ private static void setupSuperUser()
+ {
+ QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) "
+ + "VALUES ('%s', true, true, '%s')",
+ AUTH_KEYSPACE_NAME,
+ AuthKeyspace.ROLES,
+ CassandraRoleManager.DEFAULT_SUPERUSER_NAME,
+ "xxx"));
+ }
+
+ @BeforeClass
+ public static void defineSchema() throws ConfigurationException
+ {
+ SchemaLoader.prepareServer();
+
+ SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
+ new AuthTestUtils.LocalPasswordAuthenticator(),
+ new AuthTestUtils.LocalCassandraAuthorizer(),
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ cidrAuthorizer);
+ AuthCacheService.initializeAndRegisterCaches();
+ setupSuperUser();
+ }
+
+ @Before
+ public void clear()
+ {
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_GROUPS).truncateBlocking();
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_PERMISSIONS).truncateBlocking();
+ cidrAuthorizer.getCidrPermissionsCache().invalidate();
+ }
+
+ private void testValidCidrAccess(String userName, String ip)
+ {
+ InetSocketAddress ipAddr = new InetSocketAddress(ip, 0);
+
+ AuthenticatedUser user = new AuthenticatedUser(userName);
+ Assert.assertTrue(user.hasAccessFromIp(ipAddr));
+
+ ClientState clientState = ClientState.forExternalCalls(ipAddr);
+ clientState.login(user);
+ clientState.validateLogin(); // expect no exception
+ }
+
+ private void testInvalidCidrAccess(String userName, String ip)
+ {
+ InetSocketAddress ipAddr = new InetSocketAddress(ip, 0);
+
+ AuthenticatedUser user = new AuthenticatedUser(userName);
+ Assert.assertFalse(user.hasAccessFromIp(ipAddr));
+ }
+
+ private void testInvalidCidrLogin(String userName, String ip)
+ {
+ ClientState clientState = ClientState.forExternalCalls(new InetSocketAddress(ip, 0));
+ clientState.login(new AuthenticatedUser(userName));
+
+ assertThatThrownBy(clientState::validateLogin).isInstanceOf(UnauthorizedException.class).hasMessageContaining(
+ format("You do not have access from this IP %s", ip));
+ }
+
+ @Test
+ public void testCidrAccesses()
+ {
+ Map<String, List<String>> usersList = new HashMap<String, List<String>>()
+ {{
+ put("user1", Collections.singletonList("cidrGroup1"));
+ put("user2", Arrays.asList("cidrGroup2", "cidrGroup3"));
+ }};
+
+ Map<String, List<CIDR>> cidrsMapping = new HashMap<String, List<CIDR>>()
+ {{
+ put("cidrGroup1", Collections.singletonList(CIDR.getInstance("10.20.30.5/24")));
+ put("cidrGroup2", Arrays.asList(CIDR.getInstance("1111:2222:3333:4444:5555:6666:7777:8888/106"),
+ CIDR.getInstance("2001:3002::/16")));
+ put("cidrGroup3", Arrays.asList(CIDR.getInstance("40.50.60.7/32"), CIDR.getInstance("50.60.70.80/10"),
+ CIDR.getInstance("60.70.80.90/22")));
+ }};
+
+ AuthTestUtils.createUsersWithCidrAccess(usersList);
+ AuthTestUtils.insertCidrsMappings(cidrsMapping);
+
+ for (Map.Entry<String, List<String>> user : usersList.entrySet())
+ {
+ for (String cidrGroup : user.getValue())
+ {
+ List<CIDR> cidrsofCidrGroup = cidrsMapping.get(cidrGroup);
+ for (CIDR cidr : cidrsofCidrGroup)
+ {
+ testValidCidrAccess(user.getKey(), cidr.getStartIpAddress().getHostAddress());
+ }
+ }
+ }
+
+ for (CIDR cidr : cidrsMapping.get("cidrGroup2"))
+ {
+ testInvalidCidrAccess("user1", cidr.getStartIpAddress().getHostAddress());
+ }
+
+ for (CIDR cidr : cidrsMapping.get("cidrGroup3"))
+ {
+ testInvalidCidrAccess("user1", cidr.getStartIpAddress().getHostAddress());
+ }
+
+ for (CIDR cidr : cidrsMapping.get("cidrGroup1"))
+ {
+ testInvalidCidrAccess("user2", cidr.getStartIpAddress().getHostAddress());
+ }
+
+ testInvalidCidrLogin("user1", "20.30.40.6");
+ }
+
+ @Test
+ public void testNonexistingCidrLogin()
+ {
+ AuthTestUtils.createUsersWithCidrAccess(Collections.singletonMap("user11",
+ Collections.singletonList("cidrGroup11")));
+ AuthTestUtils.insertCidrsMappings(Collections.singletonMap("cidrGroup11",
+ Collections.singletonList(
+ CIDR.getInstance("200.30.40.60/24"))));
+
+ testInvalidCidrLogin("user11", "250.30.40.60");
+ }
+
+ @Test
+ public void testSuperUserAccess()
+ {
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "255.255.255.255");
+ }
+
+ @Test
+ public void testUserWithoutCidrRestrictions()
+ {
+ String roleName = "role1";
+ AuthTestUtils.auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true ",
+ roleName);
+ testValidCidrAccess(roleName, "127.0.0.1");
+ }
+
+ @Test
+ public void testUserAllowedforAllCidrs()
+ {
+ String roleName = "role1";
+ AuthTestUtils.auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true AND ACCESS FROM ALL CIDRS",
+ roleName);
+ testValidCidrAccess(roleName, "17.0.0.1");
+ }
+
+ @Test
+ public void testUserWithoutLogin()
+ {
+ String roleName = "role1";
+ AuthTestUtils.auth("CREATE ROLE %s WITH password = 'password'", roleName);
+ testInvalidCidrAccess(roleName, "17.0.0.1");
+ }
+
+ private UntypedResultSet getCidrGroups(String role)
+ {
+ String query = String.format("SELECT %s FROM %s.%s WHERE role = '%s'",
+ "cidr_groups",
+ AUTH_KEYSPACE_NAME,
+ CIDR_PERMISSIONS,
+ RoleResource.role(role).getRoleName());
+ return QueryProcessor.executeInternal(query);
+ }
+
+ @Test
+ public void testCreateAndDropRoleWithCidrsClause() throws Throwable
+ {
+ String role = "role1";
+
+ assertEmpty(getCidrGroups(role));
+
+ AuthTestUtils.createUsersWithCidrAccess(Collections.singletonMap(role,
+ Collections.singletonList("cidrGroup1")));
+ UntypedResultSet results = getCidrGroups(role);
+ Assert.assertEquals(Sets.newHashSet("cidrGroup1"),
+ Iterables.getOnlyElement(results).getFrozenSet("cidr_groups", UTF8Type.instance));
+
+ AuthTestUtils.auth("DROP ROLE %s", role);
+ assertEmpty(getCidrGroups(role));
+ }
+
+ @Test
+ public void testAlterAndDropRoleWithCidrsClause() throws Throwable
+ {
+ String role = "role1";
+
+ assertEmpty(getCidrGroups(role));
+
+ auth("CREATE ROLE %s WITH password = 'password' AND LOGIN = true ", role);
+ auth("ALTER ROLE %s WITH ACCESS FROM CIDRS {'%s'}", role, "cidrGroup1");
+
+ UntypedResultSet results = getCidrGroups(role);
+ Assert.assertEquals(Sets.newHashSet("cidrGroup1"),
+ Iterables.getOnlyElement(results).getFrozenSet("cidr_groups", UTF8Type.instance));
+
+ AuthTestUtils.auth("DROP ROLE %s", role);
+ assertEmpty(getCidrGroups(role));
+ }
+
+ @Test
+ public void testInvalidCidrs()
+ {
+ Map<String, List<String>> usersList = new HashMap<String, List<String>>()
+ {{
+ put("user1", Collections.singletonList("cidrGroup1"));
+ }};
+
+ AuthTestUtils.createUsersWithCidrAccess(usersList);
+
+ QueryProcessor.executeInternal(format(
+ "insert into %s.%s(cidr_group, cidrs) values('cidrGroup1', { ('10.20.30.5', 33), ('11.20.30.6', 16) } );",
+ AUTH_KEYSPACE_NAME, CIDR_GROUPS));
+ DatabaseDescriptor.getCIDRAuthorizer().loadCidrGroupsCache(); // update cache with CIDRs inserted above
+
+ testInvalidCidrAccess("user1", "10.20.30.5");
+ testValidCidrAccess("user1", "11.20.30.60");
+ }
+
+ @Test
+ public void testCidrChecksForSuperUsers()
+ {
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "10.20.30.5");
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "200.30.40.60");
+
+ Config conf = DatabaseDescriptor.getRawConfig();
+ conf.cidr_authorizer = new ParameterizedClass(CassandraCIDRAuthorizer.class.getName(), new HashMap<>());
+ conf.cidr_authorizer.parameters.put("cidr_checks_for_superusers", String.valueOf(true));
+
+ AuthTestUtils.insertCidrsMappings(Collections.singletonMap("cidrGroup1",
+ Collections.singletonList(
+ CIDR.getInstance("200.30.40.60/24"))));
+ AuthTestUtils.auth("alter role %s with access from cidrs {'cidrGroup1'}",
+ CassandraRoleManager.DEFAULT_SUPERUSER_NAME);
+ cidrAuthorizer.getCidrPermissionsCache().invalidate();
+
+ testInvalidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "10.20.30.5");
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "200.30.40.60");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerMonitorModeTest.java b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerMonitorModeTest.java
new file mode 100644
index 0000000000..fe92b2c886
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/CassandraCIDRAuthorizerMonitorModeTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.auth;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.HashMap;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.service.ClientState;
+
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_PERMISSIONS;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+
+public class CassandraCIDRAuthorizerMonitorModeTest extends CQLTester
+{
+ private static final AuthTestUtils.LocalCassandraCIDRAuthorizer cidrAuthorizer =
+ new AuthTestUtils.LocalCassandraCIDRAuthorizer(ICIDRAuthorizer.CIDRAuthorizerMode.MONITOR);
+ private static void setupSuperUser()
+ {
+ QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) "
+ + "VALUES ('%s', true, true, '%s')",
+ AUTH_KEYSPACE_NAME,
+ AuthKeyspace.ROLES,
+ CassandraRoleManager.DEFAULT_SUPERUSER_NAME,
+ "xxx"));
+ }
+
+ @BeforeClass
+ public static void defineSchema() throws ConfigurationException
+ {
+ SchemaLoader.prepareServer();
+
+ SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
+ new AuthTestUtils.LocalPasswordAuthenticator(),
+ new AuthTestUtils.LocalCassandraAuthorizer(),
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ cidrAuthorizer);
+ AuthCacheService.initializeAndRegisterCaches();
+ setupSuperUser();
+ }
+
+ @Before
+ public void before()
+ {
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_GROUPS).truncateBlocking();
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_PERMISSIONS).truncateBlocking();
+ cidrAuthorizer.getCidrPermissionsCache().invalidate();
+ }
+
+ private void testValidCidrAccess(String userName, String ip)
+ {
+ InetSocketAddress ipAddr = new InetSocketAddress(ip, 0);
+
+ AuthenticatedUser user = new AuthenticatedUser(userName);
+ Assert.assertTrue(user.hasAccessFromIp(ipAddr));
+
+ ClientState clientState = ClientState.forExternalCalls(ipAddr);
+ clientState.login(user);
+ clientState.validateLogin(); // expect no exception
+ }
+
+ @Test
+ public void testCidrAccesses()
+ {
+ AuthTestUtils.createUsersWithCidrAccess(Collections.singletonMap("user1",
+ Collections.singletonList("cidrGroup1")));
+ AuthTestUtils.insertCidrsMappings(Collections.singletonMap("cidrGroup1",
+ Collections.singletonList(
+ CIDR.getInstance("10.20.30.5/24"))));
+
+ testValidCidrAccess("user1", "10.20.30.5");
+ testValidCidrAccess("user1", "20.20.30.5");
+ }
+
+ @Test
+ public void testNonexistingCidrLogin()
+ {
+ AuthTestUtils.createUsersWithCidrAccess(Collections.singletonMap("user11",
+ Collections.singletonList("cidrGroup11")));
+ AuthTestUtils.insertCidrsMappings(Collections.singletonMap("cidrGroup11",
+ Collections.singletonList(
+ CIDR.getInstance("200.30.40.60/24"))));
+
+ testValidCidrAccess("user11", "250.30.40.60");
+ }
+
+ @Test
+ public void testCidrChecksForSuperUsers()
+ {
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "10.20.30.5");
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "200.30.40.60");
+
+ Config conf = DatabaseDescriptor.getRawConfig();
+ conf.cidr_authorizer = new ParameterizedClass(CassandraCIDRAuthorizer.class.getName(), new HashMap<>());
+ conf.cidr_authorizer.parameters.put("cidr_checks_for_superusers", String.valueOf(true));
+
+ AuthTestUtils.insertCidrsMappings(Collections.singletonMap("cidrGroup1",
+ Collections.singletonList(
+ CIDR.getInstance("200.30.40.60/24"))));
+ AuthTestUtils.auth("alter role %s with access from cidrs {'cidrGroup1'}",
+ CassandraRoleManager.DEFAULT_SUPERUSER_NAME);
+ cidrAuthorizer.getCidrPermissionsCache().invalidate();
+
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "10.20.30.5");
+ testValidCidrAccess(CassandraRoleManager.DEFAULT_SUPERUSER_NAME, "200.30.40.60");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java b/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java
index 3a208f8f6b..ecfa790a2e 100644
--- a/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java
+++ b/test/unit/org/apache/cassandra/auth/CassandraNetworkAuthorizerTest.java
@@ -30,23 +30,21 @@ import org.junit.Test;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
-import org.apache.cassandra.cql3.statements.AlterRoleStatement;
-import org.apache.cassandra.cql3.statements.AuthenticationStatement;
-import org.apache.cassandra.cql3.statements.CreateRoleStatement;
-import org.apache.cassandra.cql3.statements.DropRoleStatement;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.ConfigurationException;
-import org.apache.cassandra.service.ClientState;
import static org.apache.cassandra.auth.AuthKeyspace.NETWORK_PERMISSIONS;
+import static org.apache.cassandra.auth.AuthTestUtils.auth;
import static org.apache.cassandra.auth.AuthTestUtils.getRolesReadCount;
import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
-public class CassandraNetworkAuthorizerTest
+public class CassandraNetworkAuthorizerTest extends CQLTester
{
private static void setupSuperUser()
{
@@ -65,7 +63,8 @@ public class CassandraNetworkAuthorizerTest
SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
new AuthTestUtils.LocalPasswordAuthenticator(),
new AuthTestUtils.LocalCassandraAuthorizer(),
- new AuthTestUtils.LocalCassandraNetworkAuthorizer());
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ new AuthTestUtils.LocalCassandraCIDRAuthorizer());
AuthCacheService.initializeAndRegisterCaches();
setupSuperUser();
}
@@ -83,7 +82,7 @@ public class CassandraNetworkAuthorizerTest
NETWORK_PERMISSIONS,
RoleResource.role(username).getName());
UntypedResultSet results = QueryProcessor.executeInternal(query);
- Assert.assertTrue(results.isEmpty());
+ assertTrue(results != null && results.isEmpty());
}
private static void assertDcPermRow(String username, String... dcs)
@@ -94,6 +93,7 @@ public class CassandraNetworkAuthorizerTest
NETWORK_PERMISSIONS,
RoleResource.role(username).getName());
UntypedResultSet results = QueryProcessor.executeInternal(query);
+ assertNotNull(results);
UntypedResultSet.Row row = Iterables.getOnlyElement(results);
Set<String> actual = row.getFrozenSet("dcs", UTF8Type.instance);
Assert.assertEquals(expected, actual);
@@ -104,26 +104,6 @@ public class CassandraNetworkAuthorizerTest
return RandomStringUtils.randomAlphabetic(8).toLowerCase();
}
- private static ClientState getClientState()
- {
- ClientState state = ClientState.forInternalCalls();
- state.login(new AuthenticatedUser(CassandraRoleManager.DEFAULT_SUPERUSER_NAME));
- return state;
- }
-
- private static void auth(String query, Object... args)
- {
- CQLStatement statement = QueryProcessor.parseStatement(String.format(query, args)).prepare(ClientState.forInternalCalls());
- assert statement instanceof CreateRoleStatement
- || statement instanceof AlterRoleStatement
- || statement instanceof DropRoleStatement;
- AuthenticationStatement authStmt = (AuthenticationStatement) statement;
-
- // invalidate roles cache so that any changes to the underlying roles are picked up
- Roles.cache.invalidate();
- authStmt.execute(getClientState());
- }
-
private static DCPermissions dcPerms(String username)
{
AuthenticatedUser user = new AuthenticatedUser(username);
diff --git a/test/unit/org/apache/cassandra/auth/RolesTest.java b/test/unit/org/apache/cassandra/auth/RolesTest.java
index 5eced338ce..7cdbaf2856 100644
--- a/test/unit/org/apache/cassandra/auth/RolesTest.java
+++ b/test/unit/org/apache/cassandra/auth/RolesTest.java
@@ -49,7 +49,8 @@ public class RolesTest
SchemaLoader.setupAuth(roleManager,
new AuthTestUtils.LocalPasswordAuthenticator(),
new AuthTestUtils.LocalCassandraAuthorizer(),
- new AuthTestUtils.LocalCassandraNetworkAuthorizer());
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ new AuthTestUtils.LocalCassandraCIDRAuthorizer());
for (RoleResource role : ALL_ROLES)
roleManager.createRole(AuthenticatedUser.ANONYMOUS_USER, role, new RoleOptions());
diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
index 4b1890ffff..b1e9c1ec0d 100644
--- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
+++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
@@ -73,6 +73,8 @@ public class DatabaseDescriptorRefTest
"org.apache.cassandra.auth.IAuthenticator",
"org.apache.cassandra.auth.IAuthorizer",
"org.apache.cassandra.auth.IInternodeAuthenticator",
+ "org.apache.cassandra.auth.ICIDRAuthorizer",
+ "org.apache.cassandra.auth.ICIDRAuthorizer$CIDRAuthorizerMode",
"org.apache.cassandra.auth.INetworkAuthorizer",
"org.apache.cassandra.auth.IRoleManager",
"org.apache.cassandra.config.CassandraRelevantProperties",
diff --git a/test/unit/org/apache/cassandra/cql3/CIDRTest.java b/test/unit/org/apache/cassandra/cql3/CIDRTest.java
new file mode 100644
index 0000000000..7099b478f6
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/CIDRTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.cql3;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+import static org.junit.Assert.*;
+
+public class CIDRTest
+{
+ @Test
+ public void testIpv4StartingAndEndingIP()
+ {
+ CIDR cidr = CIDR.getInstance("255.255.255.25/24");
+ assertEquals("255.255.255.0/24", cidr.toString());
+ assertEquals("255.255.255.0", cidr.getStartIpAddress().getHostAddress());
+ assertEquals("255.255.255.255", cidr.getEndIpAddress().getHostAddress());
+
+ cidr = CIDR.getInstance("255.12.143.255/21");
+ assertEquals("255.12.136.0/21", cidr.toString());
+ assertEquals("255.12.136.0", cidr.getStartIpAddress().getHostAddress());
+ assertEquals("255.12.143.255", cidr.getEndIpAddress().getHostAddress());
+
+ cidr = CIDR.getInstance("255.25.55.255/31");
+ assertEquals("255.25.55.254/31", cidr.toString());
+ assertEquals("255.25.55.254", cidr.getStartIpAddress().getHostAddress());
+ assertEquals("255.25.55.255", cidr.getEndIpAddress().getHostAddress());
+
+ cidr = CIDR.getInstance("255.25.55.255/32");
+ assertEquals("255.25.55.255/32", cidr.toString());
+ assertEquals("255.25.55.255", cidr.getStartIpAddress().getHostAddress());
+ assertEquals("255.25.55.255", cidr.getEndIpAddress().getHostAddress());
+ }
+
+ @Test
+ public void testIpv6AdjustedToStartingIP()
+ {
+ CIDR cidr = CIDR.getInstance("2222:3333:4444:5555:6666:7777:8888:9999/16");
+ assertEquals("2222:0:0:0:0:0:0:0/16", cidr.toString());
+ assertEquals("2222:0:0:0:0:0:0:0", cidr.getStartIpAddress().getHostAddress());
+ assertEquals("2222:ffff:ffff:ffff:ffff:ffff:ffff:ffff", cidr.getEndIpAddress().getHostAddress());
+
+ cidr = CIDR.getInstance("2222:3333:4444:5555::/24");
+ assertEquals("2222:3300:0:0:0:0:0:0/24", cidr.toString());
+ assertEquals("2222:3300:0:0:0:0:0:0", cidr.getStartIpAddress().getHostAddress());
+ assertEquals("2222:33ff:ffff:ffff:ffff:ffff:ffff:ffff", cidr.getEndIpAddress().getHostAddress());
+
+ cidr = CIDR.getInstance("2222:3333::4444:550f/127");
+ assertEquals("2222:3333:0:0:0:0:4444:550e/127", cidr.toString());
+ assertEquals("2222:3333:0:0:0:0:4444:550e", cidr.getStartIpAddress().getHostAddress());
+ assertEquals("2222:3333:0:0:0:0:4444:550f", cidr.getEndIpAddress().getHostAddress());
+ }
+
+ @Test
+ public void testIpv4MappedIpv6()
+ {
+ CIDR cidr = CIDR.getInstance("::ffff:152.153.154.155/128");
+ assertEquals("152.153.154.155/32", cidr.toString());
+ assertEquals(32, cidr.getNetMask());
+ assertFalse(cidr.isIPv6());
+ assertTrue(cidr.isIPv4());
+ assertTrue(cidr.equals(CIDR.getInstance("152.153.154.155/32")));
+
+ cidr = CIDR.getInstance("::ffff:152.153.154.155/120");
+ assertEquals("152.153.154.0/24", cidr.toString());
+ assertEquals(24, cidr.getNetMask());
+ assertFalse(cidr.isIPv6());
+ assertTrue(cidr.isIPv4());
+ assertTrue(cidr.equals(CIDR.getInstance("152.153.154.0/24")));
+ }
+
+ @Test()
+ public void testCidrWithInvalidNetMask()
+ {
+ assertThatThrownBy(() -> CIDR.getInstance("100.100.100.100/33"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("100.100.100.100/33 is not a valid CIDR String");
+
+ assertThatThrownBy(() -> new CIDR(InetAddress.getByName("100.100.100.100"), (short)33))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid netmask 33 for IP 100.100.100.100");
+ }
+
+ @Test()
+ public void testInvalidIpv4MappedIpv6Mask()
+ {
+ assertThatThrownBy((() -> CIDR.getInstance("::ffff:152.153.154.155/90").toString()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("::ffff:152.153.154.155/90 is not a valid CIDR String");
+ }
+
+ @Test
+ public void testNullOrEmptyCidrString()
+ {
+ assertThatThrownBy((() -> CIDR.getInstance(null).toString()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("null is not a valid CIDR String");
+
+ assertThatThrownBy((() -> CIDR.getInstance("").toString()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(" is not a valid CIDR String");
+ }
+
+ @Test
+ public void testCidrStringWithoutNetMask()
+ {
+ assertThatThrownBy((() -> CIDR.getInstance("10.20.30.40").toString()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("10.20.30.40 is not a valid CIDR String");
+ }
+
+ @Test
+ public void testCidrStringWithInvalidIp()
+ {
+ assertThatThrownBy((() -> CIDR.getInstance("10.20.300.40/24").toString()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("10.20.300.40/24 is not a valid CIDR String");
+ }
+
+ @Test
+ public void testCidrAsTupleString()
+ {
+ CIDR cidr = CIDR.getInstance("10.20.30.40/24");
+ assertEquals("('10.20.30.0', 24)", cidr.asCqlTupleString());
+ }
+
+ @Test
+ public void testCompareIPs() throws UnknownHostException
+ {
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("10.20.30.40"), InetAddress.getByName("10.20.30.41")) < 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("10.20.30.40"), InetAddress.getByName("10.20.30.39")) > 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("10.20.30.40"), InetAddress.getByName("10.20.30.40")) == 0);
+
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("2222:3333::4444:550f"),
+ InetAddress.getByName("2222:5555::4444:550f")) < 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("2222:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
+ InetAddress.getByName("2222:3333::4444:550f")) > 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("2222:3333::4444:550f"),
+ InetAddress.getByName("2222:3333:0000:0000::4444:550f")) == 0);
+
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("10.20.30.40"), InetAddress.getByName("10.22.40.40")) < 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("10.24.40.40"), InetAddress.getByName("10.20.30.40")) > 0);
+
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("0.20.30.40"), InetAddress.getByName("255.20.40.40")) < 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("255.20.40.40"), InetAddress.getByName("128.20.30.40")) > 0);
+
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("::ffff:152.153.154.155"),
+ InetAddress.getByName("152.154.154.155")) < 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("::ffff:152.154.154.155"),
+ InetAddress.getByName("152.153.154.155")) > 0);
+ assertTrue(CIDR.compareIPs(InetAddress.getByName("::ffff:152.153.154.155"),
+ InetAddress.getByName("152.153.154.155")) == 0);
+ }
+
+ @Test
+ public void testCidrOverlaps()
+ {
+ assertFalse(CIDR.overlaps(CIDR.getInstance("10.20.30.0/24"), CIDR.getInstance("20.20.30.0/24")));
+ assertFalse(CIDR.overlaps(CIDR.getInstance("10.20.63.0/24"), CIDR.getInstance("10.20.79.0/24")));
+
+ assertTrue(CIDR.overlaps(CIDR.getInstance("10.20.30.0/24"), CIDR.getInstance("10.20.30.0/25")));
+ assertTrue(CIDR.overlaps(CIDR.getInstance("10.20.30.0/24"), CIDR.getInstance("10.20.30.0/24")));
+
+ assertTrue(CIDR.overlaps(CIDR.getInstance("::ffff:152.153.154.155/120"),
+ CIDR.getInstance("152.153.154.155/24")));
+ assertFalse(CIDR.overlaps(CIDR.getInstance("::ffff:152.153.154.155/120"),
+ CIDR.getInstance("::ffff:152.153.153.155/120")));
+
+ assertTrue(CIDR.overlaps(CIDR.getInstance("2222:3333:4444:5555:6666:7777:8888:9999/16"),
+ CIDR.getInstance("2222:3333:4444:5555:6666:7777:8888:9999/36")));
+ assertFalse(CIDR.overlaps(CIDR.getInstance("2222:3333:4444:5555:6666:7777:8888:9999/16"),
+ CIDR.getInstance("1010:3333:4444:5555:6666:7777:8888:9999/16")));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java
index afdcfaf54e..85a3186648 100644
--- a/test/unit/org/apache/cassandra/cql3/CQLTester.java
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -558,6 +558,7 @@ public abstract class CQLTester
DatabaseDescriptor.setAuthenticator(new AuthTestUtils.LocalPasswordAuthenticator());
DatabaseDescriptor.setAuthorizer(new AuthTestUtils.LocalCassandraAuthorizer());
DatabaseDescriptor.setNetworkAuthorizer(new AuthTestUtils.LocalCassandraNetworkAuthorizer());
+ DatabaseDescriptor.setCIDRAuthorizer(new AuthTestUtils.LocalCassandraCIDRAuthorizer());
// The CassandraRoleManager constructor set the supported and alterable options based on
// DatabaseDescriptor authenticator type so it needs to be created only after the authenticator is set.
@@ -576,6 +577,7 @@ public abstract class CQLTester
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
DatabaseDescriptor.getNetworkAuthorizer().setup();
+ DatabaseDescriptor.getCIDRAuthorizer().setup();
Schema.instance.registerListener(new AuthSchemaChangeListener());
AuthCacheService.initializeAndRegisterCaches();
diff --git a/test/unit/org/apache/cassandra/cql3/statements/AddIdentityStatementTest.java b/test/unit/org/apache/cassandra/cql3/statements/AddIdentityStatementTest.java
index e77ae35413..f90685782c 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/AddIdentityStatementTest.java
+++ b/test/unit/org/apache/cassandra/cql3/statements/AddIdentityStatementTest.java
@@ -72,7 +72,8 @@ public class AddIdentityStatementTest
SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
new AuthTestUtils.LocalPasswordAuthenticator(),
new AuthTestUtils.LocalCassandraAuthorizer(),
- new AuthTestUtils.LocalCassandraNetworkAuthorizer());
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ new AuthTestUtils.LocalCassandraCIDRAuthorizer());
AuthCacheService.initializeAndRegisterCaches();
setupPrivilegedUser();
}
diff --git a/test/unit/org/apache/cassandra/cql3/statements/AlterRoleStatementTest.java b/test/unit/org/apache/cassandra/cql3/statements/AlterRoleStatementTest.java
index 883b4f5442..3526d672bb 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/AlterRoleStatementTest.java
+++ b/test/unit/org/apache/cassandra/cql3/statements/AlterRoleStatementTest.java
@@ -22,6 +22,7 @@ import com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.Test;
+import org.apache.cassandra.auth.CIDRPermissions;
import org.apache.cassandra.auth.DCPermissions;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QueryProcessor;
@@ -40,14 +41,19 @@ public class AlterRoleStatementTest
return parse(query).dcPermissions;
}
+ private static CIDRPermissions cidrPerms(String query)
+ {
+ return parse(query).cidrPermissions;
+ }
+
@Test
- public void dcsNotSpecified() throws Exception
+ public void dcsNotSpecified()
{
Assert.assertNull(dcPerms("ALTER ROLE r1 WITH PASSWORD = 'password'"));
}
@Test
- public void dcsAllSpecified() throws Exception
+ public void dcsAllSpecified()
{
DCPermissions dcPerms = dcPerms("ALTER ROLE r1 WITH ACCESS TO ALL DATACENTERS");
Assert.assertNotNull(dcPerms);
@@ -55,7 +61,7 @@ public class AlterRoleStatementTest
}
@Test
- public void singleDc() throws Exception
+ public void singleDc()
{
DCPermissions dcPerms = dcPerms("ALTER ROLE r1 WITH ACCESS TO DATACENTERS {'dc1'}");
Assert.assertNotNull(dcPerms);
@@ -64,11 +70,43 @@ public class AlterRoleStatementTest
}
@Test
- public void multiDcs() throws Exception
+ public void multiDcs()
{
DCPermissions dcPerms = dcPerms("ALTER ROLE r1 WITH ACCESS TO DATACENTERS {'dc1', 'dc2'}");
Assert.assertNotNull(dcPerms);
Assert.assertTrue(dcPerms.restrictsAccess());
Assert.assertEquals(Sets.newHashSet("dc1", "dc2"), dcPerms.allowedDCs());
}
+
+ @Test
+ public void cidrsNotSpecified()
+ {
+ Assert.assertNull(cidrPerms("ALTER ROLE r1 WITH PASSWORD = 'password'"));
+ }
+
+ @Test
+ public void cidrsAllSpecified()
+ {
+ CIDRPermissions cidrPerms = cidrPerms("ALTER ROLE r1 WITH ACCESS FROM ALL CIDRS");
+ Assert.assertNotNull(cidrPerms);
+ Assert.assertFalse(cidrPerms.restrictsAccess());
+ }
+
+ @Test
+ public void singleCidr()
+ {
+ CIDRPermissions cidrPerms = cidrPerms("ALTER ROLE r1 WITH ACCESS FROM CIDRS {'region1'}");
+ Assert.assertNotNull(cidrPerms);
+ Assert.assertTrue(cidrPerms.restrictsAccess());
+ Assert.assertEquals(Sets.newHashSet("region1"), cidrPerms.allowedCIDRGroups());
+ }
+
+ @Test
+ public void multiCidrs()
+ {
+ CIDRPermissions cidrPerms = cidrPerms("ALTER ROLE r1 WITH ACCESS FROM CIDRS {'region1', 'region2'}");
+ Assert.assertNotNull(cidrPerms);
+ Assert.assertTrue(cidrPerms.restrictsAccess());
+ Assert.assertEquals(Sets.newHashSet("region1", "region2"), cidrPerms.allowedCIDRGroups());
+ }
}
diff --git a/test/unit/org/apache/cassandra/cql3/statements/CreateRoleStatementTest.java b/test/unit/org/apache/cassandra/cql3/statements/CreateRoleStatementTest.java
index 7a23da2d19..3dd1afd2a0 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/CreateRoleStatementTest.java
+++ b/test/unit/org/apache/cassandra/cql3/statements/CreateRoleStatementTest.java
@@ -22,6 +22,7 @@ import com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.Test;
+import org.apache.cassandra.auth.CIDRPermissions;
import org.apache.cassandra.auth.DCPermissions;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.CQLTester;
@@ -42,14 +43,19 @@ public class CreateRoleStatementTest extends CQLTester
return parse(query).dcPermissions;
}
+ private static CIDRPermissions cidrPerms(String query)
+ {
+ return parse(query).cidrPermissions;
+ }
+
@Test
- public void allDcsImplicit() throws Exception
+ public void allDcsImplicit()
{
Assert.assertFalse(dcPerms("CREATE ROLE role").restrictsAccess());
}
@Test
- public void allDcsExplicit() throws Exception
+ public void allDcsExplicit()
{
Assert.assertFalse(dcPerms("CREATE ROLE role WITH ACCESS TO ALL DATACENTERS").restrictsAccess());
}
@@ -63,11 +69,38 @@ public class CreateRoleStatementTest extends CQLTester
}
@Test
- public void multiDcs() throws Exception
+ public void multiDcs()
{
DCPermissions perms = dcPerms("CREATE ROLE role WITH ACCESS TO DATACENTERS {'dc1', 'dc2'}");
Assert.assertTrue(perms.restrictsAccess());
Assert.assertEquals(Sets.newHashSet("dc1", "dc2"), perms.allowedDCs());
+ }
+
+ @Test
+ public void allCidrsImplicit() throws Exception
+ {
+ Assert.assertFalse(cidrPerms("CREATE ROLE role").restrictsAccess());
+ }
+
+ @Test
+ public void allCidrsExplicit()
+ {
+ Assert.assertFalse(dcPerms("CREATE ROLE role WITH ACCESS FROM ALL CIDRS").restrictsAccess());
+ }
+
+ @Test
+ public void singleCidr()
+ {
+ CIDRPermissions perms = cidrPerms("CREATE ROLE role WITH ACCESS FROM CIDRS {'aodc'}");
+ Assert.assertTrue(perms.restrictsAccess());
+ Assert.assertEquals(Sets.newHashSet("aodc"), perms.allowedCIDRGroups());
+ }
+ @Test
+ public void multiCidrs()
+ {
+ CIDRPermissions perms = cidrPerms("CREATE ROLE role WITH ACCESS FROM CIDRS {'aodc', 'aws'}");
+ Assert.assertTrue(perms.restrictsAccess());
+ Assert.assertEquals(Sets.newHashSet("aodc", "aws"), perms.allowedCIDRGroups());
}
}
diff --git a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java b/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
index 51c61bbb12..325e0be233 100644
--- a/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
+++ b/test/unit/org/apache/cassandra/cql3/statements/CreateUserStatementTest.java
@@ -21,6 +21,7 @@ package org.apache.cassandra.cql3.statements;
import org.junit.Assert;
import org.junit.Test;
+import org.apache.cassandra.auth.CIDRPermissions;
import org.apache.cassandra.auth.DCPermissions;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QueryProcessor;
@@ -39,9 +40,20 @@ public class CreateUserStatementTest
return parse(query).dcPermissions;
}
+ private static CIDRPermissions cidrPerms(String query)
+ {
+ return parse(query).cidrPermissions;
+ }
+
@Test
- public void allDcsImplicit() throws Exception
+ public void allDcsImplicit()
{
Assert.assertFalse(dcPerms("CREATE USER u1").restrictsAccess());
}
+
+ @Test
+ public void allCidrsImplicit() throws Exception
+ {
+ Assert.assertFalse(cidrPerms("CREATE USER u2").restrictsAccess());
+ }
}
diff --git a/test/unit/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableTest.java
new file mode 100644
index 0000000000..889d862135
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/virtual/CIDRFilteringMetricsTableTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.virtual;
+
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.codahale.metrics.Snapshot;
+import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.auth.AuthKeyspace;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.CassandraRoleManager;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
+
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_PERMISSIONS;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class CIDRFilteringMetricsTableTest extends CQLTester
+{
+ private static final String KS_NAME = "vts";
+
+ private static void setupSuperUser()
+ {
+ QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) " +
+ "VALUES ('%s', true, true, '%s')",
+ AUTH_KEYSPACE_NAME,
+ AuthKeyspace.ROLES,
+ CassandraRoleManager.DEFAULT_SUPERUSER_NAME,
+ "xxx"));
+ }
+
+ @BeforeClass
+ public static void defineSchema() throws ConfigurationException
+ {
+ SchemaLoader.prepareServer();
+
+ SchemaLoader.setupAuth(new AuthTestUtils.LocalCassandraRoleManager(),
+ new AuthTestUtils.LocalPasswordAuthenticator(),
+ new AuthTestUtils.LocalCassandraAuthorizer(),
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ new AuthTestUtils.LocalCassandraCIDRAuthorizer());
+ AuthCacheService.initializeAndRegisterCaches();
+ setupSuperUser();
+ }
+
+ @Before
+ public void clear()
+ {
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_GROUPS).truncateBlocking();
+ Keyspace.open(AUTH_KEYSPACE_NAME).getColumnFamilyStore(CIDR_PERMISSIONS).truncateBlocking();
+ }
+
+ private void queryAndValidateCountMetrics() throws Throwable
+ {
+ CIDRAuthorizerMetrics cidrAuthorizerMetrics = DatabaseDescriptor.getCIDRAuthorizer().getCidrAuthorizerMetrics();
+
+ String getMetricsQuery = "SELECT * FROM " + KS_NAME + '.' +
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.TABLE_NAME;
+ UntypedResultSet vtsRows = execute(getMetricsQuery);
+
+ assertEquals(5, vtsRows.size());
+
+ Iterator<UntypedResultSet.Row> it = vtsRows.iterator();
+ while (it.hasNext())
+ {
+ UntypedResultSet.Row row = it.next();
+ String metricName = row.getString(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.NAME_COL);
+ long metricValue = row.getLong(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.VALUE_COL);
+ assertTrue(metricValue != 0);
+
+ if (CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.CIDR_GROUPS_CACHE_RELOAD_COUNT_NAME
+ .equals(metricName))
+ assertEquals(cidrAuthorizerMetrics.cacheReloadCount.getCount(), metricValue, 0);
+ else
+ {
+ if (metricName.contains(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable
+ .CIDR_ACCESSES_REJECTED_COUNT_NAME_PREFIX))
+ assertEquals(cidrAuthorizerMetrics.rejectedCidrAccessCount.get(metricName.split(
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable
+ .CIDR_ACCESSES_REJECTED_COUNT_NAME_PREFIX)[1]).getCount(), metricValue, 0);
+ else
+ assertEquals(cidrAuthorizerMetrics.acceptedCidrAccessCount.get(metricName.split(
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable
+ .CIDR_ACCESSES_ACCEPTED_COUNT_NAME_PREFIX)[1]).getCount(), metricValue, 0);
+ }
+ }
+ }
+
+ private void verifyLatencies(Snapshot snapshot, UntypedResultSet.Row row)
+ {
+ assertEquals(snapshot.getMedian(),
+ row.getDouble(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.P50_COL), 0.01);
+ assertEquals(snapshot.get95thPercentile(),
+ row.getDouble(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.P95_COL), 0.01);
+ assertEquals(snapshot.get99thPercentile(),
+ row.getDouble(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.P99_COL), 0.01);
+ assertEquals(snapshot.get999thPercentile(),
+ row.getDouble(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.P999_COL), 0.01);
+ assertEquals(snapshot.getMax(),
+ row.getDouble(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.MAX_COL), 0.01);
+ }
+
+ private void queryAndValidateLatencyMetrics() throws Throwable
+ {
+ CIDRAuthorizerMetrics cidrAuthorizerMetrics = DatabaseDescriptor.getCIDRAuthorizer().getCidrAuthorizerMetrics();
+
+ String getMetricsQuery = "SELECT * FROM " + KS_NAME + '.' +
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.TABLE_NAME;
+ UntypedResultSet vtsRows = execute(getMetricsQuery);
+
+ assertEquals(3, vtsRows.size());
+
+ Iterator<UntypedResultSet.Row> it = vtsRows.iterator();
+ while (it.hasNext())
+ {
+ UntypedResultSet.Row row = it.next();
+ String metricName = row.getString(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable.NAME_COL);
+
+ switch (metricName)
+ {
+ case CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.CIDR_CHECKS_LATENCY_NAME:
+ verifyLatencies(cidrAuthorizerMetrics.cidrChecksLatency.getSnapshot(), row);
+ break;
+ case CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.CIDR_GROUPS_CACHE_RELOAD_LATENCY_NAME:
+ verifyLatencies(cidrAuthorizerMetrics.cacheReloadLatency.getSnapshot(), row);
+ break;
+ case CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable.LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY_NAME:
+ verifyLatencies(cidrAuthorizerMetrics.lookupCidrGroupsForIpLatency.getSnapshot(), row);
+ break;
+ }
+ }
+ }
+
+ @Test
+ public void testCidrFilteringStats() throws Throwable
+ {
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable countsTable =
+ new CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable(KS_NAME);
+
+ CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable latenciesTable =
+ new CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable(KS_NAME);
+
+ VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(countsTable,
+ latenciesTable)));
+
+ Map<String, List<String>> usersList = new HashMap<String, List<String>>()
+ {{
+ put("user1", Collections.singletonList("cidrGroup1"));
+ put("user2", Arrays.asList("cidrGroup2", "cidrGroup3"));
+ }};
+
+ Map<String, List<CIDR>> cidrsMapping = new HashMap<String, List<CIDR>>()
+ {{
+ put("cidrGroup1", Collections.singletonList(CIDR.getInstance("10.20.30.5/24")));
+ put("cidrGroup2", Arrays.asList(CIDR.getInstance("20.30.40.6/16"), CIDR.getInstance("30.40.50.6/8")));
+ put("cidrGroup3", Arrays.asList(CIDR.getInstance("40.50.60.7/32"), CIDR.getInstance("50.60.70.80/10"),
+ CIDR.getInstance("60.70.80.90/22")));
+ }};
+
+ AuthTestUtils.createUsersWithCidrAccess(usersList);
+ AuthTestUtils.insertCidrsMappings(cidrsMapping);
+
+ AuthenticatedUser user = new AuthenticatedUser("user1");
+ assertTrue(user.hasAccessFromIp(new InetSocketAddress("10.20.30.5", 0)));
+ Assert.assertFalse(user.hasAccessFromIp(new InetSocketAddress("11.20.30.5", 0)));
+ Assert.assertFalse(user.hasAccessFromIp(new InetSocketAddress("20.30.140.60", 0)));
+ Assert.assertFalse(user.hasAccessFromIp(new InetSocketAddress("50.60.170.180", 0)));
+
+ queryAndValidateCountMetrics();
+ queryAndValidateLatencyMetrics();
+ }
+
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CIDRFilteringStatsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/CIDRFilteringStatsTest.java
new file mode 100644
index 0000000000..ec415df09b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/CIDRFilteringStatsTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.tools.nodetool;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable;
+import org.apache.cassandra.db.virtual.VirtualKeyspace;
+import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
+import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class CIDRFilteringStatsTest extends CQLTester
+{
+ @BeforeClass
+ public static void setup() throws Exception
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+ AuthCacheService.initializeAndRegisterCaches();
+
+ String KS_NAME = SchemaConstants.VIRTUAL_VIEWS;
+ VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME,
+ CIDRFilteringMetricsTable.getAll(KS_NAME)));
+
+ startJMXServer();
+ }
+
+ @Before
+ public void before()
+ {
+ AuthTestUtils.createUsersWithCidrAccess(Collections.singletonMap("user1",
+ Collections.singletonList("cidrGroup1")));
+
+ AuthTestUtils.insertCidrsMappings(Collections.singletonMap("cidrGroup1",
+ Collections.singletonList(
+ CIDR.getInstance("10.20.30.5/24"))));
+ }
+
+ @Test
+ @SuppressWarnings("SingleCharacterStringConcatenation")
+ public void testMaybeChangeDocs()
+ {
+ // If you added, modified options or help, please update docs if necessary
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "cidrfilteringstats");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool cidrfilteringstats - Print statistics on CIDR filtering\n" +
+ "\n" +
+ "SYNOPSIS\n" +
+ " nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+ " [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+ " [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+ " [(-u <username> | --username <username>)] cidrfilteringstats\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " -h <host>, --host <host>\n" +
+ " Node hostname or ip address\n" +
+ "\n" +
+ " -p <port>, --port <port>\n" +
+ " Remote jmx agent port number\n" +
+ "\n" +
+ " -pp, --print-port\n" +
+ " Operate in 4.0 mode with hosts disambiguated by port number\n" +
+ "\n" +
+ " -pw <password>, --password <password>\n" +
+ " Remote jmx agent password\n" +
+ "\n" +
+ " -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+ " Path to the JMX password file\n" +
+ "\n" +
+ " -u <username>, --username <username>\n" +
+ " Remote jmx agent username\n" +
+ "\n" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ @Test
+ public void testCIDRFilteringStats()
+ {
+ AuthenticatedUser user = new AuthenticatedUser("user1");
+ assertTrue(user.hasAccessFromIp(new InetSocketAddress("10.20.30.5", 0)));
+ assertFalse(user.hasAccessFromIp(new InetSocketAddress("11.20.30.5", 0)));
+
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("cidrfilteringstats");
+ tool.assertOnCleanExit();
+
+ String output = tool.getStdout();
+
+ assertThat(output).contains(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable
+ .CIDR_ACCESSES_ACCEPTED_COUNT_NAME_PREFIX);
+ assertThat(output).contains(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable
+ .CIDR_ACCESSES_REJECTED_COUNT_NAME_PREFIX);
+ assertThat(output).contains(CIDRFilteringMetricsTable.CIDRFilteringMetricsCountsTable
+ .CIDR_GROUPS_CACHE_RELOAD_COUNT_NAME);
+
+ assertThat(output).contains(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable
+ .CIDR_CHECKS_LATENCY_NAME);
+ assertThat(output).contains(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable
+ .CIDR_GROUPS_CACHE_RELOAD_LATENCY_NAME);
+ assertThat(output).contains(CIDRFilteringMetricsTable.CIDRFilteringMetricsLatenciesTable
+ .LOOKUP_CIDR_GROUPS_FOR_IP_LATENCY_NAME);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/DropCIDRGroupTest.java b/test/unit/org/apache/cassandra/tools/nodetool/DropCIDRGroupTest.java
new file mode 100644
index 0000000000..4ec303068f
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/DropCIDRGroupTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.tools.nodetool;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DropCIDRGroupTest extends CQLTester
+{
+ @BeforeClass
+ public static void setup() throws Exception
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+ AuthCacheService.initializeAndRegisterCaches();
+
+ startJMXServer();
+ }
+
+ @Before
+ public void insertCidrGroups()
+ {
+ Map<String, List<CIDR>> cidrsMapping = new HashMap<String, List<CIDR>>()
+ {{
+ put("test1", Arrays.asList(CIDR.getInstance("10.11.12.0/24")));
+ put("test2", Arrays.asList(CIDR.getInstance("11.11.12.0/24"), CIDR.getInstance("12.11.12.0/18")));
+ }};
+
+ AuthTestUtils.insertCidrsMappings(cidrsMapping);
+ }
+
+ @Test
+ @SuppressWarnings("SingleCharacterStringConcatenation")
+ public void testDropCidrGroupDoc()
+ {
+ // If you added, modified options or help, please update docs if necessary
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "dropcidrgroup");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool dropcidrgroup - Drop an existing cidr group\n" +
+ "\n" +
+ "SYNOPSIS\n" +
+ " nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+ " [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+ " [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+ " [(-u <username> | --username <username>)] dropcidrgroup [--] <cidrGroup>\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " -h <host>, --host <host>\n" +
+ " Node hostname or ip address\n" +
+ "\n" +
+ " -p <port>, --port <port>\n" +
+ " Remote jmx agent port number\n" +
+ "\n" +
+ " -pp, --print-port\n" +
+ " Operate in 4.0 mode with hosts disambiguated by port number\n" +
+ "\n" +
+ " -pw <password>, --password <password>\n" +
+ " Remote jmx agent password\n" +
+ "\n" +
+ " -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+ " Path to the JMX password file\n" +
+ "\n" +
+ " -u <username>, --username <username>\n" +
+ " Remote jmx agent username\n" +
+ "\n" +
+ " --\n" +
+ " This option can be used to separate command-line options from the\n" +
+ " list of argument, (useful when arguments might be mistaken for\n" +
+ " command-line options\n" +
+ "\n" +
+ " <cidrGroup>\n" +
+ " Requires a cidr group name\n" +
+ "\n" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ @Test
+ public void testDropCidrGroup()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("listcidrgroups");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("test1");
+ assertThat(tool.getStdout()).contains("test2");
+
+ tool = ToolRunner.invokeNodetool("dropcidrgroup", "test1");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("Deleted CIDR group test1");
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("test2");
+
+ tool = ToolRunner.invokeNodetool("dropcidrgroup", "test2");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("Deleted CIDR group test2");
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+ }
+
+ @Test
+ public void testDropNonExistingCidrGroup()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("dropcidrgroup", "unknown");
+ assertThat(tool.getStderr()).contains("CIDR group 'unknown' doesn't exists");
+ assertThat(tool.getStdout()).doesNotContain("Deleted CIDR group");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIPTest.java b/test/unit/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIPTest.java
new file mode 100644
index 0000000000..e8844053c3
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/GetCIDRGroupsOfIPTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.tools.nodetool;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GetCIDRGroupsOfIPTest extends CQLTester
+{
+ @BeforeClass
+ public static void setup() throws Exception
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+ AuthCacheService.initializeAndRegisterCaches();
+
+ startJMXServer();
+ }
+
+ @Before
+ public void before()
+ {
+ Map<String, List<CIDR>> cidrsMapping = new HashMap<String, List<CIDR>>()
+ {{
+ put("test1", Arrays.asList(CIDR.getInstance("10.11.12.0/24")));
+ put("test2", Arrays.asList(CIDR.getInstance("11.11.12.0/24"), CIDR.getInstance("12.11.12.0/18")));
+ }};
+
+ AuthTestUtils.insertCidrsMappings(cidrsMapping);
+ }
+
+ @Test
+ @SuppressWarnings("SingleCharacterStringConcatenation")
+ public void testGetCidrGroupsOfIpDoc()
+ {
+ // If you added, modified options or help, please update docs if necessary
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "getcidrgroupsofip");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool getcidrgroupsofip - Print CIDR groups associated with given IP\n" +
+ "\n" +
+ "SYNOPSIS\n" +
+ " nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+ " [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+ " [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+ " [(-u <username> | --username <username>)] getcidrgroupsofip [--] <IP\n" +
+ " address>\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " -h <host>, --host <host>\n" +
+ " Node hostname or ip address\n" +
+ "\n" +
+ " -p <port>, --port <port>\n" +
+ " Remote jmx agent port number\n" +
+ "\n" +
+ " -pp, --print-port\n" +
+ " Operate in 4.0 mode with hosts disambiguated by port number\n" +
+ "\n" +
+ " -pw <password>, --password <password>\n" +
+ " Remote jmx agent password\n" +
+ "\n" +
+ " -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+ " Path to the JMX password file\n" +
+ "\n" +
+ " -u <username>, --username <username>\n" +
+ " Remote jmx agent username\n" +
+ "\n" +
+ " --\n" +
+ " This option can be used to separate command-line options from the\n" +
+ " list of argument, (useful when arguments might be mistaken for\n" +
+ " command-line options\n" +
+ "\n" +
+ " <IP address>\n" +
+ " Requires IP address as a string\n" +
+ "\n" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ @Test
+ public void testGetCidrGroupsOfIp()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("getcidrgroupsofip", "10.11.12.1");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("test1");
+
+ tool = ToolRunner.invokeNodetool("getcidrgroupsofip", "11.11.12.12");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("test2");
+
+ tool = ToolRunner.invokeNodetool("getcidrgroupsofip", "12.11.12.12");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("test2");;
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCacheTest.java
new file mode 100644
index 0000000000..90be844b63
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/InvalidateCIDRPermissionsCacheTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.tools.nodetool;
+
+import java.net.InetSocketAddress;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.IRoleManager;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static org.apache.cassandra.auth.AuthTestUtils.ROLE_A;
+import static org.apache.cassandra.auth.AuthTestUtils.ROLE_B;
+import static org.apache.cassandra.auth.AuthTestUtils.getCidrPermissionsReadCount;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InvalidateCIDRPermissionsCacheTest extends CQLTester
+{
+ static InetSocketAddress ipAddr;
+
+ @BeforeClass
+ public static void setup() throws Exception
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+
+ IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
+ roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions());
+ roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions());
+ AuthCacheService.initializeAndRegisterCaches();
+
+ startJMXServer();
+
+ ipAddr = new InetSocketAddress("127.0.0.0", 0);
+ }
+
+ @Test
+ @SuppressWarnings("SingleCharacterStringConcatenation")
+ public void testMaybeChangeDocs()
+ {
+ // If you added, modified options or help, please update docs if necessary
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "invalidatecidrpermissionscache");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool invalidatecidrpermissionscache - Invalidate the cidr\n" +
+ " permissions cache\n" +
+ "\n" +
+ "SYNOPSIS\n" +
+ " nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+ " [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+ " [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+ " [(-u <username> | --username <username>)] invalidatecidrpermissionscache\n" +
+ " [--] [<role>...]\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " -h <host>, --host <host>\n" +
+ " Node hostname or ip address\n" +
+ "\n" +
+ " -p <port>, --port <port>\n" +
+ " Remote jmx agent port number\n" +
+ "\n" +
+ " -pp, --print-port\n" +
+ " Operate in 4.0 mode with hosts disambiguated by port number\n" +
+ "\n" +
+ " -pw <password>, --password <password>\n" +
+ " Remote jmx agent password\n" +
+ "\n" +
+ " -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+ " Path to the JMX password file\n" +
+ "\n" +
+ " -u <username>, --username <username>\n" +
+ " Remote jmx agent username\n" +
+ "\n" +
+ " --\n" +
+ " This option can be used to separate command-line options from the\n" +
+ " list of argument, (useful when arguments might be mistaken for\n" +
+ " command-line options\n" +
+ "\n" +
+ " [<role>...]\n" +
+ " List of roles to invalidate. By default, all roles\n" +
+ "\n" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ @Test
+ public void testInvalidateSingleCidrPermission()
+ {
+ AuthenticatedUser role = new AuthenticatedUser(ROLE_A.getRoleName());
+
+ // cache cidr permission
+ role.hasAccessFromIp(ipAddr);
+ long originalReadsCount = getCidrPermissionsReadCount();
+
+ // ensure cidr permission is cached
+ role.hasAccessFromIp(ipAddr);
+ assertThat(originalReadsCount).isEqualTo(getCidrPermissionsReadCount());
+
+ // invalidate cidr permission
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("invalidatecidrpermissionscache", ROLE_A.getRoleName());
+ tool.assertOnCleanExit();
+
+ // ensure cidr permission is reloaded
+ assertThat(role.hasAccessFromIp(new InetSocketAddress("127.0.0.0", 0))).isTrue();
+ }
+
+ @Test
+ public void testInvalidateAllCidrPermissions()
+ {
+ AuthenticatedUser roleA = new AuthenticatedUser(ROLE_A.getRoleName());
+ AuthenticatedUser roleB = new AuthenticatedUser(ROLE_B.getRoleName());
+
+ // cache cidr permissions
+ roleA.hasAccessFromIp(ipAddr);
+ roleB.hasAccessFromIp(ipAddr);
+ long originalReadsCount = getCidrPermissionsReadCount();
+
+ // enure cidr permissions are cached
+ assertThat(roleA.hasAccessFromIp(ipAddr)).isTrue();
+ assertThat(roleB.hasAccessFromIp(ipAddr)).isTrue();
+ assertThat(originalReadsCount).isEqualTo(getCidrPermissionsReadCount());
+
+ // invalidate both cidr permissions
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("invalidatecidrpermissionscache");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("Invalidated CIDR permissions cache");
+
+ // ensure cidr permission for roleA is reloaded
+ assertThat(roleA.hasAccessFromIp(ipAddr)).isTrue();
+ long readsCountAfterFirstReLoad = getCidrPermissionsReadCount();
+ assertThat(originalReadsCount).isLessThan(readsCountAfterFirstReLoad);
+
+ // ensure cidr permission for roleB is reloaded
+ assertThat(roleB.hasAccessFromIp(ipAddr)).isTrue();
+ long readsCountAfterSecondReLoad = getCidrPermissionsReadCount();
+ assertThat(readsCountAfterFirstReLoad).isLessThan(readsCountAfterSecondReLoad);
+ }
+
+ @Test
+ public void testInvalidateNonExistingRole()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("invalidatecidrpermissionscache", "role1");
+ assertThat(tool.getStdout()).contains("Not found role role1 in CIDR permissions cache, nothing to invalidate");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ListCIDRGroupTest.java b/test/unit/org/apache/cassandra/tools/nodetool/ListCIDRGroupTest.java
new file mode 100644
index 0000000000..a6b377d9dc
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/ListCIDRGroupTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.tools.nodetool;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.cql3.CIDR;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static org.apache.cassandra.auth.AuthTestUtils.insertCidrsMappings;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ListCIDRGroupTest extends CQLTester
+{
+ @BeforeClass
+ public static void setup() throws Exception
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+ AuthCacheService.initializeAndRegisterCaches();
+
+ startJMXServer();
+ }
+
+ @Before
+ public void before()
+ {
+ Map<String, List<CIDR>> cidrsMapping = new HashMap<String, List<CIDR>>()
+ {{
+ put("test1", Collections.singletonList(CIDR.getInstance("10.11.12.0/24")));
+ put("test2", Arrays.asList(CIDR.getInstance("11.11.12.0/24"), CIDR.getInstance("12.11.12.0/18")));
+ }};
+
+ insertCidrsMappings(cidrsMapping);
+ }
+
+ @Test
+ @SuppressWarnings("SingleCharacterStringConcatenation")
+ public void testListCidrGroupDoc()
+ {
+ // If you added, modified options or help, please update docs if necessary
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "listcidrgroups");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool listcidrgroups - List existing cidr groups\n" +
+ "\n" +
+ "SYNOPSIS\n" +
+ " nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+ " [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+ " [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+ " [(-u <username> | --username <username>)] listcidrgroups [--]\n" +
+ " [<cidrGroup>]\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " -h <host>, --host <host>\n" +
+ " Node hostname or ip address\n" +
+ "\n" +
+ " -p <port>, --port <port>\n" +
+ " Remote jmx agent port number\n" +
+ "\n" +
+ " -pp, --print-port\n" +
+ " Operate in 4.0 mode with hosts disambiguated by port number\n" +
+ "\n" +
+ " -pw <password>, --password <password>\n" +
+ " Remote jmx agent password\n" +
+ "\n" +
+ " -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+ " Path to the JMX password file\n" +
+ "\n" +
+ " -u <username>, --username <username>\n" +
+ " Remote jmx agent username\n" +
+ "\n" +
+ " --\n" +
+ " This option can be used to separate command-line options from the\n" +
+ " list of argument, (useful when arguments might be mistaken for\n" +
+ " command-line options\n" +
+ "\n" +
+ " [<cidrGroup>]\n" +
+ " LIST operation can be invoked with or without cidr group name\n" +
+ "\n" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ @Test
+ public void testListCidrGroup()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("listcidrgroups");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("test1");
+ assertThat(tool.getStdout()).contains("test2");
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups", "test1");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("10.11.12.0/24");
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups", "test2");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("11.11.12.0/24");
+ assertThat(tool.getStdout()).contains("12.11.0.0/18");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCacheTest.java b/test/unit/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCacheTest.java
new file mode 100644
index 0000000000..3237d77c32
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/ReloadCIDRGroupsCacheTest.java
@@ -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.tools.nodetool;
+
+import java.net.InetSocketAddress;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.auth.AuthTestUtils;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.IRoleManager;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static java.lang.String.format;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
+import static org.apache.cassandra.auth.AuthTestUtils.ROLE_A;
+import static org.apache.cassandra.auth.AuthTestUtils.ROLE_B;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReloadCIDRGroupsCacheTest extends CQLTester
+{
+ static InetSocketAddress ipAddr;
+
+ @BeforeClass
+ public static void setup() throws Exception
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+
+ IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
+ roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions());
+ roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions());
+ AuthCacheService.initializeAndRegisterCaches();
+
+ startJMXServer();
+
+ ipAddr = new InetSocketAddress("127.0.0.0", 0);
+ }
+
+ @Before
+ public void before() throws Throwable
+ {
+ execute(format("insert into %s.%s (cidr_group, cidrs) values('test1', { ('10.11.12.0', 24) })",
+ AUTH_KEYSPACE_NAME, CIDR_GROUPS));
+ }
+
+ @Test
+ @SuppressWarnings("SingleCharacterStringConcatenation")
+ public void testMaybeChangeDocs()
+ {
+ // If you added, modified options or help, please update docs if necessary
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "reloadcidrgroupscache");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool reloadcidrgroupscache - Reload CIDR groups cache with latest\n" +
+ " entries in cidr_groups table, when CIDR authorizer is enabled\n" +
+ "\n" +
+ "SYNOPSIS\n" +
+ " nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+ " [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+ " [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+ " [(-u <username> | --username <username>)] reloadcidrgroupscache\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " -h <host>, --host <host>\n" +
+ " Node hostname or ip address\n" +
+ "\n" +
+ " -p <port>, --port <port>\n" +
+ " Remote jmx agent port number\n" +
+ "\n" +
+ " -pp, --print-port\n" +
+ " Operate in 4.0 mode with hosts disambiguated by port number\n" +
+ "\n" +
+ " -pw <password>, --password <password>\n" +
+ " Remote jmx agent password\n" +
+ "\n" +
+ " -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+ " Path to the JMX password file\n" +
+ "\n" +
+ " -u <username>, --username <username>\n" +
+ " Remote jmx agent username\n" +
+ "\n" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ private long getCidrGroupsReadCount()
+ {
+ return DatabaseDescriptor.getCIDRAuthorizer().getCidrAuthorizerMetrics().cacheReloadCount.getCount();
+ }
+
+ @Test
+ public void testReloadCidrGroupsCache()
+ {
+ AuthenticatedUser role = new AuthenticatedUser(ROLE_A.getRoleName());
+
+ long originalReadsCount = getCidrGroupsReadCount();
+
+ // ensure cidr groups cache not reloaded
+ role.hasAccessFromIp(ipAddr);
+ assertThat(originalReadsCount).isEqualTo(getCidrGroupsReadCount());
+
+ // invalidate all, which reloads cidr groups cache
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("reloadcidrgroupscache");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("Reloaded CIDR groups cache");
+
+ // ensure cidr groups cache reloaded
+ role.hasAccessFromIp(ipAddr);
+ assertThat(originalReadsCount).isLessThan(getCidrGroupsReadCount());
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/UpdateCIDRGroupTest.java b/test/unit/org/apache/cassandra/tools/nodetool/UpdateCIDRGroupTest.java
new file mode 100644
index 0000000000..b3fbb9d34d
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/UpdateCIDRGroupTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.tools.nodetool;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.auth.AuthCacheService;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static java.lang.String.format;
+import static org.apache.cassandra.auth.AuthKeyspace.CIDR_GROUPS;
+import static org.apache.cassandra.schema.SchemaConstants.AUTH_KEYSPACE_NAME;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UpdateCIDRGroupTest extends CQLTester
+{
+ @BeforeClass
+ public static void setup() throws Exception
+ {
+ CQLTester.setUpClass();
+ CQLTester.requireAuthentication();
+ AuthCacheService.initializeAndRegisterCaches();
+
+ startJMXServer();
+ }
+
+ @Before
+ public void cleanCidrGroups() throws Throwable
+ {
+ execute(format("TRUNCATE %s.%s", AUTH_KEYSPACE_NAME, CIDR_GROUPS));
+ }
+
+ @Test
+ @SuppressWarnings("SingleCharacterStringConcatenation")
+ public void testUpdateCidrGroupDoc()
+ {
+ // If you added, modified options or help, please update docs if necessary
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("help", "updatecidrgroup");
+ tool.assertOnCleanExit();
+
+ String help = "NAME\n" +
+ " nodetool updatecidrgroup - Insert/Update a cidr group\n" +
+ "\n" +
+ "SYNOPSIS\n" +
+ " nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]\n" +
+ " [(-pp | --print-port)] [(-pw <password> | --password <password>)]\n" +
+ " [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]\n" +
+ " [(-u <username> | --username <username>)] updatecidrgroup [--]\n" +
+ " [<cidrGroup> <cidr> ...]\n" +
+ "\n" +
+ "OPTIONS\n" +
+ " -h <host>, --host <host>\n" +
+ " Node hostname or ip address\n" +
+ "\n" +
+ " -p <port>, --port <port>\n" +
+ " Remote jmx agent port number\n" +
+ "\n" +
+ " -pp, --print-port\n" +
+ " Operate in 4.0 mode with hosts disambiguated by port number\n" +
+ "\n" +
+ " -pw <password>, --password <password>\n" +
+ " Remote jmx agent password\n" +
+ "\n" +
+ " -pwf <passwordFilePath>, --password-file <passwordFilePath>\n" +
+ " Path to the JMX password file\n" +
+ "\n" +
+ " -u <username>, --username <username>\n" +
+ " Remote jmx agent username\n" +
+ "\n" +
+ " --\n" +
+ " This option can be used to separate command-line options from the\n" +
+ " list of argument, (useful when arguments might be mistaken for\n" +
+ " command-line options\n" +
+ "\n" +
+ " [<cidrGroup> <cidr> ...]\n" +
+ " Requires a cidr group name, followed by one or more CIDRs separated\n" +
+ " by space\n" +
+ "\n" +
+ "\n";
+ assertThat(tool.getStdout()).isEqualTo(help);
+ }
+
+ @Test
+ public void testUpdateCidrGroup()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("listcidrgroups");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+
+ tool = ToolRunner.invokeNodetool("updatecidrgroup", "test1", "10.11.12.0/24");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+
+ tool = ToolRunner.invokeNodetool("updatecidrgroup", "test2", "11.11.12.0/24", "12.11.0.0/18");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("test1");
+ assertThat(tool.getStdout()).contains("test2");
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups", "test1");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("10.11.12.0/24");
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups", "test2");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("11.11.12.0/24");
+ assertThat(tool.getStdout()).contains("12.11.0.0/18");
+ }
+
+ @Test
+ public void testUpdateSameCidrGroup()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("listcidrgroups");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+
+ tool = ToolRunner.invokeNodetool("updatecidrgroup", "test", "10.11.12.0/24");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups", "test");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("10.11.12.0/24");
+
+ tool = ToolRunner.invokeNodetool("updatecidrgroup", "test", "11.11.12.0/24");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups", "test");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).contains("11.11.12.0/24");
+ }
+
+ @Test
+ public void testUpdateInvalidCidrGroup()
+ {
+ ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("updatecidrgroup", "test", "10.11.12.0/33");
+ assertThat(tool.getStderr()).contains("is not a valid CIDR String");
+
+ tool = ToolRunner.invokeNodetool("listcidrgroups", "test");
+ tool.assertOnCleanExit();
+ assertThat(tool.getStdout()).isEmpty();
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java b/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java
index 47ca5febf2..1dc7a30e26 100644
--- a/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java
+++ b/test/unit/org/apache/cassandra/tools/nodetool/VerifyTest.java
@@ -41,7 +41,8 @@ public class VerifyTest extends CQLTester
SchemaLoader.setupAuth(roleManager,
new AuthTestUtils.LocalPasswordAuthenticator(),
new AuthTestUtils.LocalCassandraAuthorizer(),
- new AuthTestUtils.LocalCassandraNetworkAuthorizer());
+ new AuthTestUtils.LocalCassandraNetworkAuthorizer(),
+ new AuthTestUtils.LocalCassandraCIDRAuthorizer());
roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_A, AuthTestUtils.getLoginRoleOptions());
roleManager.createRole(AuthenticatedUser.SYSTEM_USER, ROLE_B, AuthTestUtils.getLoginRoleOptions());
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org