the test was skipped for tablets because CDC wasn't supported with tablets, but now it is supported and the issue is closed, so the test should be unskipped.
866 lines
55 KiB
Python
866 lines
55 KiB
Python
# Copyright 2021-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
|
|
#############################################################################
|
|
# Tests for managing permissions
|
|
#############################################################################
|
|
|
|
import pytest
|
|
import time
|
|
from . import rest_api
|
|
from cassandra.protocol import SyntaxException, InvalidRequest, Unauthorized, ConfigurationException
|
|
from .util import new_test_table, new_function, new_user, new_session, new_test_keyspace, unique_name, new_type, new_materialized_view, is_scylla
|
|
from contextlib import contextmanager
|
|
|
|
# Figure out which keyspace contains the roles table (its location changed
|
|
# across different releases of Scylla and Cassandra)
|
|
def get_roles_table(cql):
|
|
for ks in ['system', 'system_auth_v2', 'system_auth']:
|
|
try:
|
|
t = ks + '.roles'
|
|
cql.execute(f'SELECT * FROM {t}')
|
|
# If we're still here, the SELECT didn't fail
|
|
return t
|
|
except:
|
|
pass
|
|
pytest.fail("Couldn't find roles table")
|
|
|
|
# Test that granting permissions to various resources works for the default user.
|
|
# This case does not include functions, because due to differences in implementation
|
|
# the tests will diverge between Scylla and Cassandra (e.g. there's no common language)
|
|
# to create a user-defined function in.
|
|
# Marked cassandra_bug, because Cassandra allows granting a DESCRIBE permission
|
|
# to a specific ROLE, in contradiction to its own documentation:
|
|
# https://cassandra.apache.org/doc/latest/cassandra/cql/security.html#cql-permissions
|
|
def test_grant_applicable_data_and_role_permissions(cql, test_keyspace, cassandra_bug):
|
|
schema = "a int primary key"
|
|
user = "cassandra"
|
|
with new_test_table(cql, test_keyspace, schema) as table:
|
|
# EXECUTE is not listed, as it only applies to functions, which aren't covered in this test case
|
|
all_permissions = set(['create', 'alter', 'drop', 'select', 'modify', 'authorize', 'describe'])
|
|
applicable_permissions = {
|
|
'all keyspaces': ['create', 'alter', 'drop', 'select', 'modify', 'authorize'],
|
|
f'keyspace {test_keyspace}': ['create', 'alter', 'drop', 'select', 'modify', 'authorize'],
|
|
f'table {table}': ['alter', 'drop', 'select', 'modify', 'authorize'],
|
|
'all roles': ['create', 'alter', 'drop', 'authorize', 'describe'],
|
|
f'role {user}': ['alter', 'drop', 'authorize'],
|
|
}
|
|
for resource, permissions in applicable_permissions.items():
|
|
# Applicable permissions can be legally granted
|
|
for permission in permissions:
|
|
cql.execute(f"GRANT {permission} ON {resource} TO {user}")
|
|
# Only applicable permissions can be granted - nonsensical combinations
|
|
# are refused with an error
|
|
for permission in all_permissions.difference(set(permissions)):
|
|
with pytest.raises((InvalidRequest, SyntaxException), match="support.*permissions"):
|
|
cql.execute(f"GRANT {permission} ON {resource} TO {user}")
|
|
|
|
|
|
def eventually_authorized(fun, timeout_s=10):
|
|
for i in range(timeout_s * 10):
|
|
try:
|
|
return fun()
|
|
except Unauthorized as e:
|
|
time.sleep(0.1)
|
|
return fun()
|
|
|
|
def eventually_unauthorized(fun, timeout_s=10):
|
|
for i in range(timeout_s * 10):
|
|
try:
|
|
fun()
|
|
time.sleep(0.1)
|
|
except Unauthorized as e:
|
|
return
|
|
try:
|
|
fun()
|
|
pytest.fail(f"Function {fun} was not refused as unauthorized")
|
|
except Unauthorized as e:
|
|
return
|
|
|
|
# Some tests can't use eventually_authorized/unauthorized() because they
|
|
# need to check that GRANT or REVOKE does not change the behavior, so can't
|
|
# wait for it to change. So these tests need a way to make sure that the
|
|
# old permissions are no longer cached. These tests can use this function.
|
|
def ensure_updated_permissions(cql):
|
|
if is_scylla(cql):
|
|
rest_api.post_request(cql, "authorization_cache/reset")
|
|
else:
|
|
time.sleep(4)
|
|
|
|
def grant(cql, permission, resource, username):
|
|
cql.execute(f"GRANT {permission} ON {resource} TO {username}")
|
|
|
|
def revoke(cql, permission, resource, username):
|
|
cql.execute(f"REVOKE {permission} ON {resource} FROM {username}")
|
|
|
|
# Helper function for checking that given `function` can only be executed
|
|
# with given `permission` granted, and returns an unauthorized error
|
|
# with the `permission` revoked.
|
|
def check_enforced(cql, username, permission, resource, function):
|
|
eventually_unauthorized(function)
|
|
grant(cql, permission, resource, username)
|
|
eventually_authorized(function)
|
|
revoke(cql, permission, resource, username)
|
|
eventually_unauthorized(function)
|
|
|
|
def test_user_displays_as_authenticated(cql):
|
|
with new_session(cql, "cassandra") as user_session_1:
|
|
with new_session(cql, "cassandra") as user_session_2:
|
|
with new_session(cql, "cassandra") as user_session_3:
|
|
# Every opened session should create around 5 connections,
|
|
# so we'll have 3x5=15 opened connections,
|
|
# and we expect that their status will be READY,
|
|
# but, on rare occasions, it may happen that
|
|
# the connection's status is still stuck at ESTABLISHED or AUTHENTICATING,
|
|
# and we must handle this case as well,
|
|
# so we simply sleep and retry later.
|
|
for retry in range(0, 5):
|
|
res = user_session_1.execute("SELECT connection_stage FROM system.clients")
|
|
if all([r[0] == "READY" for r in res]):
|
|
return
|
|
else:
|
|
time.sleep(2)
|
|
assert False
|
|
|
|
# Test that data permissions can be granted and revoked, and that they're effective
|
|
def test_grant_revoke_data_permissions(cql, test_keyspace):
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
ks = unique_name()
|
|
# Permissions on all keyspaces
|
|
def create_keyspace_idempotent():
|
|
user_session.execute(f"CREATE KEYSPACE IF NOT EXISTS {ks} WITH REPLICATION = {{ 'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1 }}")
|
|
user_session.execute(f"DROP KEYSPACE IF EXISTS {ks}")
|
|
check_enforced(cql, username, permission='CREATE', resource='ALL KEYSPACES', function=create_keyspace_idempotent)
|
|
# Permissions for a specific keyspace
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1 }") as keyspace:
|
|
t = unique_name()
|
|
def create_table_idempotent():
|
|
user_session.execute(f"CREATE TABLE IF NOT EXISTS {keyspace}.{t}(id int primary key)")
|
|
cql.execute(f"DROP TABLE IF EXISTS {keyspace}.{t}")
|
|
eventually_unauthorized(create_table_idempotent)
|
|
grant(cql, 'CREATE', f'KEYSPACE {keyspace}', username)
|
|
eventually_authorized(create_table_idempotent)
|
|
cql.execute(f"CREATE TABLE {keyspace}.{t}(id int primary key)")
|
|
# Permissions for a specific table
|
|
check_enforced(cql, username, permission='ALTER', resource=f'{keyspace}.{t}',
|
|
function=lambda: user_session.execute(f"ALTER TABLE {keyspace}.{t} WITH comment = 'hey'"))
|
|
check_enforced(cql, username, permission='SELECT', resource=f'{keyspace}.{t}',
|
|
function=lambda: user_session.execute(f"SELECT * FROM {keyspace}.{t}"))
|
|
check_enforced(cql, username, permission='MODIFY', resource=f'{keyspace}.{t}',
|
|
function=lambda: user_session.execute(f"INSERT INTO {keyspace}.{t}(id) VALUES (42)"))
|
|
cql.execute(f"DROP TABLE {keyspace}.{t}")
|
|
revoke(cql, 'CREATE', f'KEYSPACE {keyspace}', username)
|
|
eventually_unauthorized(create_table_idempotent)
|
|
|
|
def drop_table_idempotent():
|
|
user_session.execute(f"DROP TABLE IF EXISTS {keyspace}.{t}")
|
|
cql.execute(f"CREATE TABLE IF NOT EXISTS {keyspace}.{t}(id int primary key)")
|
|
|
|
cql.execute(f"CREATE TABLE {keyspace}.{t}(id int primary key)")
|
|
check_enforced(cql, username, permission='DROP', resource=f'KEYSPACE {keyspace}', function=drop_table_idempotent)
|
|
cql.execute(f"DROP TABLE {keyspace}.{t}")
|
|
|
|
# CREATE permission on all keyspaces also implies creating any tables in any keyspace
|
|
check_enforced(cql, username, permission='CREATE', resource='ALL KEYSPACES', function=create_table_idempotent)
|
|
# Same for DROP
|
|
cql.execute(f"CREATE TABLE IF NOT EXISTS {keyspace}.{t}(id int primary key)")
|
|
check_enforced(cql, username, permission='DROP', resource='ALL KEYSPACES', function=drop_table_idempotent)
|
|
|
|
# Test that permissions for user-defined functions are serialized in a Cassandra-compatible way
|
|
def test_udf_permissions_serialization(cql):
|
|
schema = "a int primary key"
|
|
user = "cassandra"
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace, new_user(cql) as user:
|
|
with new_test_table(cql, keyspace, schema) as table:
|
|
# Creating a bilingual function makes this test case work for both Scylla and Cassandra
|
|
div_body_lua = "(b bigint, i int) CALLED ON NULL INPUT RETURNS bigint LANGUAGE lua AS 'return b//i'"
|
|
div_body_java = "(b bigint, i int) CALLED ON NULL INPUT RETURNS bigint LANGUAGE java AS 'return b/i;'"
|
|
div_body = div_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, div_body) as div_fun:
|
|
pass
|
|
except:
|
|
div_body = div_body_java
|
|
with new_function(cql, keyspace, div_body) as div_fun:
|
|
applicable_permissions = {
|
|
'all functions': ['create', 'alter', 'drop', 'authorize', 'execute'],
|
|
f'all functions in keyspace {keyspace}': ['create', 'alter', 'drop', 'authorize', 'execute'],
|
|
f'function {keyspace}.{div_fun}(bigint, int)': ['alter', 'drop', 'authorize', 'execute'],
|
|
}
|
|
for resource, permissions in applicable_permissions.items():
|
|
# Applicable permissions can be legally granted
|
|
for permission in permissions:
|
|
cql.execute(f"GRANT {permission} ON {resource} TO {user}")
|
|
|
|
permissions = {row.resource: row.permissions for row in cql.execute(f"SELECT * FROM system.role_permissions")}
|
|
assert permissions['functions'] == set(['ALTER', 'AUTHORIZE', 'CREATE', 'DROP', 'EXECUTE'])
|
|
assert permissions[f'functions/{keyspace}'] == set(['ALTER', 'AUTHORIZE', 'CREATE', 'DROP', 'EXECUTE'])
|
|
assert permissions[f'functions/{keyspace}/{div_fun}[org.apache.cassandra.db.marshal.LongType^org.apache.cassandra.db.marshal.Int32Type]'] == set(['ALTER', 'AUTHORIZE', 'DROP', 'EXECUTE'])
|
|
|
|
resources_with_execute = [row.resource for row in cql.execute(f"LIST EXECUTE OF {user}")]
|
|
assert '<all functions>' in resources_with_execute
|
|
assert f'<all functions in {keyspace}>' in resources_with_execute
|
|
assert f'<function {keyspace}.{div_fun}(bigint, int)>' in resources_with_execute
|
|
|
|
# Test that names that require quoting (e.g. due to having nonorthodox characters)
|
|
# are properly handled, with right permissions granted.
|
|
# Cassandra doesn't quote names properly, so the test fails
|
|
def test_udf_permissions_quoted_names(cassandra_bug, cql):
|
|
udt_name = f'"{unique_name()}weird_udt[t^t]a^b^[]"'
|
|
schema = f"a frozen<{udt_name}> primary key"
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_type(cql, keyspace, "(a text, b int)", udt_name) as udt, new_test_table(cql, keyspace, schema) as table:
|
|
fun_body_lua = f"(i {udt}) CALLED ON NULL INPUT RETURNS bigint LANGUAGE lua AS 'return 42;'"
|
|
fun_body_java = f"(i {udt}) CALLED ON NULL INPUT RETURNS bigint LANGUAGE java AS 'return 42;'"
|
|
fun_body = fun_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, fun_body):
|
|
pass
|
|
except:
|
|
fun_body = fun_body_java
|
|
with new_function(cql, keyspace, fun_body, f'"{unique_name()}weird[name1^name2]x^y"') as weird_fun:
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
grant(cql, 'EXECUTE', f'FUNCTION {keyspace}.{weird_fun}({udt})', username)
|
|
grant(cql, 'SELECT', table, username)
|
|
cql.execute(f"INSERT INTO {table}(a) VALUES ({{a:'hello', b:42}})")
|
|
|
|
assert list([r[0] for r in user_session.execute(f"SELECT {keyspace}.{weird_fun}(a) FROM {table}")]) == [42]
|
|
|
|
resources_with_execute = [row.resource for row in cql.execute(f"LIST EXECUTE OF {username}")]
|
|
assert f'<function {keyspace}.{weird_fun}(frozen<{udt_name}>)>' in resources_with_execute
|
|
|
|
revoke(cql, 'EXECUTE', f'FUNCTION {keyspace}.{weird_fun}({udt})', username)
|
|
check_enforced(cql, username, 'EXECUTE', f'FUNCTION {keyspace}.{weird_fun}({udt})',
|
|
lambda: user_session.execute(f"SELECT {keyspace}.{weird_fun}(a) FROM {table}"))
|
|
|
|
# Test that dropping a function without specifying its signature works with the DROP permission if there's only
|
|
# one function with the given name, and that it fails if there are multiple functions with the same name,
|
|
# regardless of the permissions of the user.
|
|
# If the signature is specified, test that the permission check is performed as usual.
|
|
# This test is marked cassandra_bug because it exposes a small error-path
|
|
# bug in Cassandra: When trying to drop an overloaded function without
|
|
# specifying which overload is intended, this error should be reported,
|
|
# regardless of whether some of the overloads have or don't have drop
|
|
# permissions. Cassandra erroneously reports the unrelated missing permissions.
|
|
# Reported to Cassandra as CASSANDRA-19005.
|
|
def test_drop_udf_with_same_name(cql, cassandra_bug):
|
|
schema = "a int primary key"
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
body1_lua = "(i int) CALLED ON NULL INPUT RETURNS bigint LANGUAGE lua AS 'return 42;'"
|
|
body1_java = "(i int) CALLED ON NULL INPUT RETURNS bigint LANGUAGE java AS 'return 42L;'"
|
|
body2_lua = "(i int, j int) CALLED ON NULL INPUT RETURNS bigint LANGUAGE lua AS 'return 42;'"
|
|
body2_java = "(i int, j int) CALLED ON NULL INPUT RETURNS bigint LANGUAGE java AS 'return 42L;'"
|
|
body1 = body1_lua
|
|
body2 = body2_lua
|
|
try:
|
|
with new_function(cql, keyspace, body1):
|
|
pass
|
|
except:
|
|
body1 = body1_java
|
|
body2 = body2_java
|
|
fun = "fun43"
|
|
cql.execute(f"CREATE FUNCTION {keyspace}.{fun}{body1}")
|
|
cql.execute(f"CREATE FUNCTION {keyspace}.{fun}{body2}")
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
grant(cql, 'DROP', f'FUNCTION {keyspace}.{fun}(int)', username)
|
|
# When trying to drop a function without a signature but
|
|
# there is more than one function with that name, we should
|
|
# get an InvalidRequest, as is confirmed by the test
|
|
# test_udf.py::test_drop_udf_with_same_name.
|
|
# Cassandra fails here because it has a bug: It decided to
|
|
# complain about the true (but irrelevant) fact that one of
|
|
# the two functions doesn't have drop permissions and throws
|
|
# Unauthorized instead of InvalidRequest.
|
|
with pytest.raises(InvalidRequest):
|
|
user_session.execute(f"DROP FUNCTION {keyspace}.{fun}")
|
|
eventually_unauthorized(lambda: user_session.execute(f"DROP FUNCTION {keyspace}.{fun}(int, int)"))
|
|
grant(cql, 'DROP', f'FUNCTION {keyspace}.{fun}(int, int)', username)
|
|
with pytest.raises(InvalidRequest):
|
|
user_session.execute(f"DROP FUNCTION {keyspace}.{fun}")
|
|
eventually_authorized(lambda: user_session.execute(f"DROP FUNCTION {keyspace}.{fun}(int)"))
|
|
eventually_authorized(lambda: user_session.execute(f"DROP FUNCTION {keyspace}.{fun}"))
|
|
|
|
# Test that permissions set for user-defined functions are enforced
|
|
# Tests for ALTER are separate, because they are qualified as cassandra_bug
|
|
def test_grant_revoke_udf_permissions(cql):
|
|
schema = "a int primary key, b list<int>"
|
|
user = "cassandra"
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_test_table(cql, keyspace, schema) as table:
|
|
fun_body_lua = "(i int, l list<int>) CALLED ON NULL INPUT RETURNS int LANGUAGE lua AS 'return 42;'"
|
|
fun_body_java = "(i int, l list<int>) CALLED ON NULL INPUT RETURNS int LANGUAGE java AS 'return 42;'"
|
|
fun_body = fun_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, fun_body) as fun:
|
|
pass
|
|
except:
|
|
fun_body = fun_body_java
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
fun = "fun42"
|
|
|
|
def create_function_idempotent():
|
|
user_session.execute(f"CREATE FUNCTION IF NOT EXISTS {keyspace}.{fun} {fun_body}")
|
|
cql.execute(f"DROP FUNCTION IF EXISTS {keyspace}.{fun}(int, list<int>)")
|
|
check_enforced(cql, username, permission='CREATE', resource=f'all functions in keyspace {keyspace}',
|
|
function=create_function_idempotent)
|
|
check_enforced(cql, username, permission='CREATE', resource='all functions',
|
|
function=create_function_idempotent)
|
|
|
|
def drop_function_idempotent():
|
|
user_session.execute(f"DROP FUNCTION IF EXISTS {keyspace}.{fun}(int, list<int>)")
|
|
cql.execute(f"CREATE FUNCTION IF NOT EXISTS {keyspace}.{fun} {fun_body}")
|
|
for resource in [f'function {keyspace}.{fun}(int, list<int>)', f'all functions in keyspace {keyspace}', 'all functions']:
|
|
check_enforced(cql, username, permission='DROP', resource=resource, function=drop_function_idempotent)
|
|
|
|
grant(cql, 'SELECT', table, username)
|
|
for resource in [f'function {keyspace}.{fun}(int, list<int>)', f'all functions in keyspace {keyspace}', 'all functions']:
|
|
check_enforced(cql, username, permission='EXECUTE', resource=resource,
|
|
function=lambda: user_session.execute(f"SELECT {keyspace}.{fun}(a, b) FROM {table}"))
|
|
|
|
grant(cql, 'EXECUTE', 'ALL FUNCTIONS', username)
|
|
def grant_idempotent():
|
|
grant(user_session, 'EXECUTE', f'function {keyspace}.{fun}(int, list<int>)', 'cassandra')
|
|
revoke(cql, 'EXECUTE', f'function {keyspace}.{fun}(int, list<int>)', 'cassandra')
|
|
for resource in [f'function {keyspace}.{fun}(int, list<int>)', f'all functions in keyspace {keyspace}', 'all functions']:
|
|
check_enforced(cql, username, permission='AUTHORIZE', resource=resource, function=grant_idempotent)
|
|
|
|
# This test case is artificially extracted from the one above,
|
|
# because it's qualified as cassandra_bug - the documentation quotes that ALTER is needed on
|
|
# functions if the definition is replaced (CREATE OR REPLACE FUNCTION (...)),
|
|
# and yet it's not enforced
|
|
def test_grant_revoke_alter_udf_permissions(cassandra_bug, cql):
|
|
schema = "a int primary key"
|
|
user = "cassandra"
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'SimpleStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_test_table(cql, keyspace, schema) as table:
|
|
fun_body_lua = "(i int) CALLED ON NULL INPUT RETURNS int LANGUAGE lua AS 'return 42;'"
|
|
fun_body_java = "(i int) CALLED ON NULL INPUT RETURNS int LANGUAGE java AS 'return 42;'"
|
|
fun_body = fun_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, fun_body) as fun:
|
|
pass
|
|
except:
|
|
fun_body = fun_body_java
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
fun = "fun42"
|
|
|
|
grant(cql, 'ALTER', 'ALL FUNCTIONS', username)
|
|
check_enforced(cql, username, permission='CREATE', resource=f'all functions in keyspace {keyspace}',
|
|
function=lambda: user_session.execute(f"CREATE OR REPLACE FUNCTION {keyspace}.{fun} {fun_body}"))
|
|
check_enforced(cql, username, permission='CREATE', resource='all functions',
|
|
function=lambda: user_session.execute(f"CREATE OR REPLACE FUNCTION {keyspace}.{fun} {fun_body}"))
|
|
revoke(cql, 'ALTER', 'ALL FUNCTIONS', username)
|
|
|
|
grant(cql, 'CREATE', 'ALL FUNCTIONS', username)
|
|
check_enforced(cql, username, permission='ALTER', resource=f'all functions in keyspace {keyspace}',
|
|
function=lambda: user_session.execute(f"CREATE OR REPLACE FUNCTION {keyspace}.{fun} {fun_body}"))
|
|
check_enforced(cql, username, permission='ALTER', resource='all functions',
|
|
function=lambda: user_session.execute(f"CREATE OR REPLACE FUNCTION {keyspace}.{fun} {fun_body}"))
|
|
check_enforced(cql, username, permission='ALTER', resource=f'FUNCTION {keyspace}.{fun}(int)',
|
|
function=lambda: user_session.execute(f"CREATE OR REPLACE FUNCTION {keyspace}.{fun} {fun_body}"))
|
|
|
|
# Test that granting permissions on non-existent UDFs fails
|
|
# This test is marked cassandra_bug because it exposes a small error-path
|
|
# bug in their parser CASSANDRA-19006 (see comment below).
|
|
def test_grant_perms_on_nonexistent_udf(cql, cassandra_bug):
|
|
keyspace = "ks"
|
|
fun_name = "fun42"
|
|
with new_user(cql) as username:
|
|
grant(cql, 'EXECUTE', 'ALL FUNCTIONS', username)
|
|
revoke(cql, 'EXECUTE', 'ALL FUNCTIONS', username)
|
|
with pytest.raises(InvalidRequest):
|
|
grant(cql, 'EXECUTE', f'ALL FUNCTIONS IN KEYSPACE {keyspace}', username)
|
|
# When the keyspace is non-existent, Scylla returns InvalidRequest
|
|
# and Cassandra returns ConfigurationException. Let's allow both.
|
|
with pytest.raises((InvalidRequest, ConfigurationException)):
|
|
grant(cql, 'EXECUTE', f'FUNCTION {keyspace}.{fun_name}(int)', username)
|
|
cql.execute(f"CREATE KEYSPACE IF NOT EXISTS {keyspace} WITH REPLICATION = {{ 'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1 }}")
|
|
grant(cql, 'EXECUTE', f'ALL FUNCTIONS IN KEYSPACE {keyspace}', username)
|
|
revoke(cql, 'EXECUTE', f'ALL FUNCTIONS IN KEYSPACE {keyspace}', username)
|
|
# When the keyspace exists, but the function doesn't, we should
|
|
# return an InvalidRequest, complaining that "function ks.fun42(int)>
|
|
# doesn't exist.". Cassandra has a bug here - CASSANDRA-19006 - it
|
|
# returns a SyntaxException with the message "NoSuchElementException
|
|
# No value present", suggesting a bug in their parser.
|
|
with pytest.raises(InvalidRequest):
|
|
grant(cql, 'EXECUTE', f'FUNCTION {keyspace}.{fun_name}(int)', username)
|
|
|
|
fun_body_lua = "(i int) CALLED ON NULL INPUT RETURNS int LANGUAGE lua AS 'return 42;'"
|
|
fun_body_java = "(i int) CALLED ON NULL INPUT RETURNS int LANGUAGE java AS 'return 42;'"
|
|
fun_body = fun_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, fun_body) as fun:
|
|
pass
|
|
except:
|
|
fun_body = fun_body_java
|
|
with new_function(cql, keyspace, fun_body, fun_name):
|
|
grant(cql, 'EXECUTE', f'FUNCTION {keyspace}.{fun_name}(int)', username)
|
|
cql.execute(f"DROP KEYSPACE IF EXISTS {keyspace}")
|
|
|
|
# Test that permissions for user-defined aggregates are also enforced.
|
|
# scylla_only, because Lua is used as the target language
|
|
def test_grant_revoke_uda_permissions(scylla_only, cql):
|
|
schema = 'id bigint primary key'
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_test_table(cql, keyspace, schema) as table:
|
|
for i in range(8):
|
|
cql.execute(f"INSERT INTO {table} (id) VALUES ({10**i})")
|
|
avg_partial_body = "(state tuple<bigint, bigint>, val bigint) CALLED ON NULL INPUT RETURNS tuple<bigint, bigint> LANGUAGE lua AS 'return {state[1] + val, state[2] + 1}'"
|
|
div_body = "(state tuple<bigint, bigint>) CALLED ON NULL INPUT RETURNS bigint LANGUAGE lua AS 'return state[1]//state[2]'"
|
|
with new_function(cql, keyspace, avg_partial_body) as avg_partial, new_function(cql, keyspace, div_body) as div_fun:
|
|
custom_avg_body = f"(bigint) SFUNC {avg_partial} STYPE tuple<bigint, bigint> FINALFUNC {div_fun} INITCOND (0,0)"
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
custom_avg = "custom_avg"
|
|
def create_aggr_idempotent():
|
|
user_session.execute(f"CREATE AGGREGATE IF NOT EXISTS {keyspace}.{custom_avg} {custom_avg_body}")
|
|
cql.execute(f"DROP AGGREGATE IF EXISTS {keyspace}.{custom_avg}(bigint)")
|
|
grant(cql, 'EXECUTE', f'function {keyspace}.{avg_partial}(tuple<bigint, bigint>, bigint)', username)
|
|
grant(cql, 'EXECUTE', f'function {keyspace}.{div_fun}(tuple<bigint, bigint>)', username)
|
|
check_enforced(cql, username, permission='CREATE', resource=f'all functions in keyspace {keyspace}',
|
|
function=create_aggr_idempotent)
|
|
check_enforced(cql, username, permission='CREATE', resource='all functions',
|
|
function=create_aggr_idempotent)
|
|
|
|
grant(cql, 'CREATE', 'ALL FUNCTIONS', username)
|
|
check_enforced(cql, username, permission='ALTER', resource=f'all functions in keyspace {keyspace}',
|
|
function=lambda: user_session.execute(f"CREATE OR REPLACE AGGREGATE {keyspace}.{custom_avg} {custom_avg_body}"))
|
|
check_enforced(cql, username, permission='ALTER', resource='all functions',
|
|
function=lambda: user_session.execute(f"CREATE OR REPLACE AGGREGATE {keyspace}.{custom_avg} {custom_avg_body}"))
|
|
|
|
grant(cql, 'SELECT', table, username)
|
|
for resource in [f'function {keyspace}.{custom_avg}(bigint)', f'all functions in keyspace {keyspace}', 'all functions']:
|
|
check_enforced(cql, username, permission='EXECUTE', resource=resource,
|
|
function=lambda: user_session.execute(f"SELECT {keyspace}.{custom_avg}(id) FROM {table}"))
|
|
|
|
def drop_aggr_idempotent():
|
|
user_session.execute(f"DROP AGGREGATE IF EXISTS {keyspace}.{custom_avg}(bigint)")
|
|
cql.execute(f"CREATE AGGREGATE IF NOT EXISTS {keyspace}.{custom_avg} {custom_avg_body}")
|
|
for resource in [f'function {keyspace}.{custom_avg}(bigint)', f'all functions in keyspace {keyspace}', 'all functions']:
|
|
check_enforced(cql, username, permission='DROP', resource=resource, function=drop_aggr_idempotent)
|
|
|
|
grant(cql, 'EXECUTE', 'ALL FUNCTIONS', username)
|
|
def grant_idempotent():
|
|
grant(user_session, 'EXECUTE', f'function {keyspace}.{custom_avg}(bigint)', 'cassandra')
|
|
revoke(cql, 'EXECUTE', f'function {keyspace}.{custom_avg}(bigint)', 'cassandra')
|
|
for resource in [f'function {keyspace}.{custom_avg}(bigint)', f'all functions in keyspace {keyspace}', 'all functions']:
|
|
check_enforced(cql, username, permission='AUTHORIZE', resource=resource, function=grant_idempotent)
|
|
|
|
cql.execute(f"DROP AGGREGATE IF EXISTS {keyspace}.{custom_avg}(bigint)")
|
|
|
|
# Test that permissions for user-defined functions created on top of user-defined types work
|
|
def test_udf_permissions_with_udt(cql):
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_type(cql, keyspace, '(v int)') as udt:
|
|
schema = f"a frozen<{udt}> primary key"
|
|
with new_test_table(cql, keyspace, schema) as table:
|
|
fun_body_lua = f"(i {udt}) CALLED ON NULL INPUT RETURNS int LANGUAGE lua AS 'return 42;'"
|
|
fun_body_java = f"(i {udt}) CALLED ON NULL INPUT RETURNS int LANGUAGE java AS 'return 42;'"
|
|
fun_body = fun_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, fun_body) as fun:
|
|
pass
|
|
except:
|
|
fun_body = fun_body_java
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
with new_function(cql, keyspace, fun_body) as fun:
|
|
cql.execute(f"INSERT INTO {table}(a) VALUES ((7))")
|
|
grant(cql, 'SELECT', table, username)
|
|
grant(cql, 'EXECUTE', f'FUNCTION {keyspace}.{fun}({udt})', username)
|
|
user_session.execute(f'SELECT {keyspace}.{fun}(a) FROM {table}')
|
|
|
|
# Test that permissions on user-defined functions with no arguments work
|
|
def test_udf_permissions_no_args(cql):
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_test_table(cql, keyspace, schema="a int primary key") as table, new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
fun_body_lua = f"() CALLED ON NULL INPUT RETURNS int LANGUAGE lua AS 'return 42;'"
|
|
fun_body_java = f"() CALLED ON NULL INPUT RETURNS int LANGUAGE java AS 'return 42;'"
|
|
fun_body = fun_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, fun_body):
|
|
pass
|
|
except:
|
|
fun_body = fun_body_java
|
|
with new_function(cql, keyspace, fun_body) as fun:
|
|
grant(cql, 'SELECT', table, username)
|
|
check_enforced(cql, username, permission='EXECUTE', resource=f'function {keyspace}.{fun}()',
|
|
function=lambda: user_session.execute(f'SELECT {keyspace}.{fun}() FROM {table}'))
|
|
with pytest.raises(SyntaxException):
|
|
nonexistent_func = unique_name()
|
|
user_session.execute(f'GRANT SELECT ON FUNCTION {keyspace}.{nonexistent_func}() TO cassandra')
|
|
|
|
# Test that the create permission can't be granted on a single function. Similarly to how we handle Roles and Tables,
|
|
# we do not allow permissions on non-existent Functions, so the CREATE permission on a specific function is meaningless,
|
|
# because granting it would only be allowed if the function was already created before.
|
|
# Reproduces #13822
|
|
def test_create_on_single_function(cql):
|
|
schema = "a int primary key"
|
|
user = "cassandra"
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_test_table(cql, keyspace, schema) as table:
|
|
fun_body_lua = f"(a int, b int) CALLED ON NULL INPUT RETURNS int LANGUAGE lua AS 'return a + b;'"
|
|
fun_body_java = f"(a int, b int) CALLED ON NULL INPUT RETURNS int LANGUAGE java AS 'return a + b;'"
|
|
fun_body = fun_body_lua
|
|
try:
|
|
with new_function(cql, keyspace, fun_body):
|
|
pass
|
|
except:
|
|
fun_body = fun_body_java
|
|
with new_function(cql, keyspace, fun_body) as fun:
|
|
with pytest.raises(SyntaxException):
|
|
grant(cql, 'CREATE', f'FUNCTION {keyspace}.{fun}(int, int)', user)
|
|
|
|
# Test that user can be granted permissions to change the password for a superuser. This behavior matches Cassandra's.
|
|
# Reproduces ScyllaDB OSS #14277
|
|
def test_grant_all_allows_superuser_password_change(cql):
|
|
with new_user(cql) as username:
|
|
with new_user(cql, with_superuser_privileges=True) as superuser:
|
|
grant(cql, 'ALTER', 'ALL ROLES', username)
|
|
with new_session(cql, username) as user_session:
|
|
user_session.execute(f"ALTER ROLE {superuser} WITH PASSWORD = '{superuser}2'")
|
|
|
|
# Reproduces https://github.com/scylladb/scylla-enterprise/issues/5210,
|
|
# i.e. a CVE reported for Cassandra https://www.cve.org/CVERecord?id=CVE-2025-23015,
|
|
def test_non_superuser_with_modify_all_keyspaces_permissions_cannot_modify_system_keyspaces(cql, test_keyspace):
|
|
roles_table = get_roles_table(cql)
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
with new_test_table(cql, test_keyspace, "a int, PRIMARY KEY (a)") as ks_cf:
|
|
# a non-superuser CANNOT modify any random table in any random keyspace
|
|
eventually_unauthorized(lambda: user_session.execute(f"INSERT INTO {ks_cf} (a) VALUES (1)"))
|
|
# unless we grant him a MODIFY permission to ALL KEYSPACES
|
|
grant(cql, 'MODIFY', 'ALL KEYSPACES', username)
|
|
# with which he can now MODIFY it
|
|
eventually_authorized(lambda: user_session.execute(f"INSERT INTO {ks_cf} (a) VALUES (1)"))
|
|
|
|
# but even an authenticated non-superuser with MODIFY on ALL KEYSPACES permissions
|
|
# cannot update system tables, especially elevate itself to a superuser
|
|
eventually_unauthorized(lambda: user_session.execute(f"UPDATE {roles_table} SET is_superuser = True WHERE role = '{username}'"))
|
|
|
|
# but an authenticated superuser user can update system tables
|
|
# (note that `cql` is a session for the already authenticated "cassandra" superuser)
|
|
cql.execute(f"UPDATE {roles_table} SET is_superuser = True WHERE role = '{username}'")
|
|
|
|
# Test that permissions on a table are not granted to a user as a creator if the table already exists when the user
|
|
# tries to create it. Reproduces GHSA-ww5v-p45p-3vhq
|
|
def test_create_on_existing_table(cql):
|
|
schema = "a int primary key"
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
with new_test_keyspace(cql, "WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
grant(cql, 'CREATE', f"KEYSPACE {keyspace}", username)
|
|
# Wait until the CREATE permission appears in the permissions cache
|
|
ensure_updated_permissions(cql)
|
|
with new_test_table(cql, keyspace, schema) as table:
|
|
def ensure_all_table_permissions_unauthorized(user_session):
|
|
def ensure_unauthorized(fun):
|
|
try:
|
|
fun()
|
|
pytest.fail(f"Function {fun} was not refused as unauthorized")
|
|
except Unauthorized:
|
|
pass
|
|
ensure_unauthorized(lambda: user_session.execute(f"ALTER TABLE {table} WITH comment = 'hey'"))
|
|
ensure_unauthorized(lambda: user_session.execute(f"SELECT * FROM {table}"))
|
|
ensure_unauthorized(lambda: user_session.execute(f"INSERT INTO {table}(a) VALUES (42)"))
|
|
ensure_unauthorized(lambda: user_session.execute(f"DROP TABLE {table}"))
|
|
# Grant the SELECT permission to the user so that the GRANT SELECT TO cassandra below only requires
|
|
# the missing AUTHORIZE permission. Wait until the permissions cache registers the SELECT permission,
|
|
# and then revoke it after confirming the user doesn't have the AUTHORIZE permission.
|
|
grant(cql, 'SELECT', table, username)
|
|
ensure_updated_permissions(cql)
|
|
rest_api.post_request(cql, "authorization_cache/reset")
|
|
ensure_unauthorized(lambda: user_session.execute(f"GRANT SELECT ON {table} TO cassandra"))
|
|
revoke(cql, 'SELECT', table, username)
|
|
|
|
ensure_all_table_permissions_unauthorized(user_session)
|
|
try:
|
|
user_session.execute(f"CREATE TABLE {table}(a int primary key)")
|
|
except:
|
|
pass
|
|
# As a result of the CREATE query, user could be granted invalid permissions but they may still be not
|
|
# visible in the permissions cache. Sleep until permissions cache is refreshed.
|
|
ensure_updated_permissions(cql)
|
|
ensure_all_table_permissions_unauthorized(user_session)
|
|
user_session.execute(f"CREATE TABLE IF NOT EXISTS {table}(a int primary key)")
|
|
ensure_updated_permissions(cql)
|
|
ensure_all_table_permissions_unauthorized(user_session)
|
|
|
|
# Test that native functions permissions are always implicitly granted.
|
|
# every user with SELECT permission for a table should be able to use
|
|
# the native functions (non UDF/UDA functions)
|
|
# ref: https://github.com/scylladb/scylladb/issues/16526
|
|
def test_native_functions_always_executable(cql):
|
|
schema = "a int primary key"
|
|
with new_test_keyspace(cql,"WITH REPLICATION = { 'class': 'NetworkTopologyStrategy', 'replication_factor': 1 }") as keyspace:
|
|
with new_test_table(cql,keyspace,schema) as table:
|
|
cql.execute(f'INSERT INTO {table}(a) VALUES(15)')
|
|
cql.execute(f'INSERT INTO {table}(a) VALUES(3)')
|
|
cql.execute(f'INSERT INTO {table}(a) VALUES(84)')
|
|
with new_user(cql) as username:
|
|
grant(cql, 'SELECT', table, username)
|
|
with new_session(cql,username) as user_session:
|
|
assert list(user_session.execute(f"SELECT count(*) FROM {table}")) == [(3,)]
|
|
assert list(user_session.execute(f"SELECT max(a) FROM {table}")) == [(84,)]
|
|
assert list(user_session.execute(f"SELECT min(a) FROM {table}")) == [(3,)]
|
|
assert list(user_session.execute(f"SELECT sum(a) FROM {table}")) == [(102,)]
|
|
|
|
@contextmanager
|
|
def eventually_new_named_table(cql, table, schema, extra=""):
|
|
eventually_authorized(lambda: cql.execute(f'CREATE TABLE {table} ({schema}) {extra}'))
|
|
try:
|
|
yield table
|
|
finally:
|
|
eventually_authorized(lambda: cql.execute(f'DROP TABLE {table}'))
|
|
|
|
# When a role has permissions to create a table, the role is given full
|
|
# access to the table it just created (we call this feature "auto-grant").
|
|
# The following tests check the auto-grant feature. The first test checks
|
|
# that the base table created by CREATE TABLE is usable by the role creating
|
|
# it. The second test is about the view created by CREATE MATERIALIZED VIEW
|
|
# being accessible. The third test is about newly created CDC being accessible.
|
|
def test_auto_grant_base(cql, test_keyspace):
|
|
with new_user(cql) as username:
|
|
# Grant username CREATE permissions on test_keyspace, but no other
|
|
# permissions:
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, username)
|
|
with new_session(cql, username) as user_session:
|
|
schema = "p int primary key"
|
|
table = f'{test_keyspace}.{unique_name()}'
|
|
with eventually_new_named_table(user_session, table, schema):
|
|
# Check that the username was auto-granted SELECT permissions
|
|
eventually_authorized(lambda: user_session.execute(f'SELECT * FROM {table}'))
|
|
# Check that the username was auto-granted MODIFY permissions
|
|
eventually_authorized(lambda: user_session.execute(f'INSERT INTO {table}(p) VALUES(42)'))
|
|
# Check that the username was auto-granted ALTER permissions
|
|
eventually_authorized(lambda: user_session.execute(f"ALTER TABLE {table} WITH comment = 'hey'"))
|
|
# When this scope ends, table is dropped in user_session,
|
|
# so we check that DROP permissions were auto-granted as well.
|
|
|
|
# Test that a role creating a materialized view is automatically granted
|
|
# permissions to read it.
|
|
def test_auto_grant_view(cql, test_keyspace):
|
|
with new_user(cql) as username:
|
|
# Grant username CREATE permissions on test_keyspace, but no other
|
|
# permissions:
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, username)
|
|
with new_session(cql, username) as user_session:
|
|
schema = "p int primary key, v int"
|
|
table = f'{test_keyspace}.{unique_name()}'
|
|
with eventually_new_named_table(user_session, table, schema):
|
|
# Because username has CREATE permissions, it can also use
|
|
# CREATE MATERIALIZED VIEW
|
|
mv = f'{test_keyspace}.{unique_name()}'
|
|
eventually_authorized(lambda: user_session.execute(f'CREATE MATERIALIZED VIEW {mv} AS SELECT * FROM {table} WHERE p IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, p)'))
|
|
# Check that the username was auto-granted SELECT permissions
|
|
# on the newly created materialized view:
|
|
eventually_authorized(lambda: user_session.execute(f'SELECT * FROM {mv}'))
|
|
# Check that the username was auto-granted ALTER permissions
|
|
# on the materialized view
|
|
eventually_authorized(lambda: user_session.execute(f"ALTER MATERIALIZED VIEW {mv} WITH comment = 'hey'"))
|
|
# Check that the username was auto-granted DROP permissions
|
|
# on the materialized view
|
|
eventually_authorized(lambda: user_session.execute(f'DROP MATERIALIZED VIEW {mv}'))
|
|
|
|
# Test that a role creating CDC is automatically granted permissions to access
|
|
# the CDC log table.
|
|
# This test has two variants - cdc_on_create=True creates the table immediately
|
|
# with CDC enabled, while cdc_on_create=False creates a table first and only
|
|
# then enables CDC. Both should work.
|
|
# This is a scylla_only test because it tests the Scylla-only CDC feature.
|
|
# Reproduces #19798:
|
|
@pytest.mark.parametrize("cdc_on_create", [True, False])
|
|
def test_auto_grant_cdc(cql, test_keyspace, cdc_on_create, scylla_only):
|
|
with new_user(cql) as username:
|
|
# Grant username CREATE permissions on test_keyspace, but no other
|
|
# permissions:
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, username)
|
|
with new_session(cql, username) as user_session:
|
|
schema = "p int primary key"
|
|
extra = "with cdc = {'enabled': true}"
|
|
table = f'{test_keyspace}.{unique_name()}'
|
|
with eventually_new_named_table(user_session, table, schema, extra if cdc_on_create else ''):
|
|
# If cdc_on_create=False, we created the table without CDC
|
|
# enabled and we'll enable it now. username should be allowed
|
|
# to do it (it is auto-granted ALTER permissions on the table
|
|
# it created)
|
|
if not cdc_on_create:
|
|
eventually_authorized(lambda: user_session.execute(f'ALTER TABLE {table} {extra}'))
|
|
# Check that the username was auto-granted SELECT permissions
|
|
# on the newly created CDC log
|
|
# Note: While this test is xfailing, we use a lower timeout
|
|
# here to avoid a very slow failure. When the test begins
|
|
# to pass, the timeout_s doesn't matter and can be removed.
|
|
eventually_authorized(lambda: user_session.execute(f'SELECT * FROM {table}_scylla_cdc_log'), timeout_s=1)
|
|
|
|
# In the above tests we checked the "auto-grant" feature - where a user that
|
|
# creates table is automatically granted permissions on the new table, views
|
|
# and CDC logs it created. If auto-grant is implemented, it is also critical
|
|
# that auto-revoke is implemented - i.e., permissions are revoked when a
|
|
# table, view or CDC log is deleted. If we forget to do that, it is possible
|
|
# that a first user creates a table/view/cdc, deletes it, and when a second
|
|
# user reuses the same name, the first user wrongly gets permission to access
|
|
# the second user's data.
|
|
# The goal of the following tests is the check this auto-revoke feature.
|
|
|
|
def test_auto_revoke_base(cql, test_keyspace):
|
|
with new_user(cql) as user1, new_user(cql) as user2:
|
|
# Grant user1 and user2 CREATE permissions on test_keyspace, but no
|
|
# other permissions:
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, user1)
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, user2)
|
|
with new_session(cql, user1) as user1_session, new_session(cql, user2) as user2_session:
|
|
schema = "p int primary key"
|
|
table = f'{test_keyspace}.{unique_name()}'
|
|
with eventually_new_named_table(user1_session, table, schema):
|
|
# A minimal check that auto-grant actually worked for user1,
|
|
# user2 can't read the table.
|
|
eventually_authorized(lambda: user1_session.execute(f'SELECT * FROM {table}'))
|
|
eventually_unauthorized(lambda: user2_session.execute(f'SELECT * FROM {table}'))
|
|
# At this point, table was deleted by user1, so user2 can create
|
|
# a table with the same name "table".
|
|
with eventually_new_named_table(user2_session, table, schema):
|
|
# Now, permissions were auto-granted for user2, but user1
|
|
# no longer has permissions on this table.
|
|
eventually_authorized(lambda: user2_session.execute(f'SELECT * FROM {table}'))
|
|
eventually_unauthorized(lambda: user1_session.execute(f'SELECT * FROM {table}'))
|
|
|
|
def test_auto_revoke_view(cql, test_keyspace):
|
|
with new_user(cql) as user1, new_user(cql) as user2:
|
|
# Grant user1 and user2 CREATE permissions on test_keyspace, but no
|
|
# other permissions:
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, user1)
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, user2)
|
|
with new_session(cql, user1) as user1_session, new_session(cql, user2) as user2_session:
|
|
schema = "p int primary key, v int"
|
|
mv = f'{test_keyspace}.{unique_name()}'
|
|
table = f'{test_keyspace}.{unique_name()}'
|
|
with eventually_new_named_table(user1_session, table, schema):
|
|
# Create a view with a pre-chosen name mv as user1:
|
|
eventually_authorized(lambda: user1_session.execute(f'CREATE MATERIALIZED VIEW {mv} AS SELECT * FROM {table} WHERE p IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, p)'))
|
|
# user1 was auto-granted permissions to read this view, user2
|
|
# was not:
|
|
eventually_authorized(lambda: user1_session.execute(f'SELECT * FROM {mv}'))
|
|
eventually_unauthorized(lambda: user2_session.execute(f'SELECT * FROM {mv}'))
|
|
eventually_authorized(lambda: user1_session.execute(f'DROP MATERIALIZED VIEW {mv}'))
|
|
# After user1 dropped the view with name mv, we let user2
|
|
# create a different table but reuse the same view name mv.
|
|
# We want to check that user1 no longer has permissions on
|
|
# this view:
|
|
table2 = f'{test_keyspace}.{unique_name()}'
|
|
with eventually_new_named_table(user2_session, table2, schema):
|
|
eventually_authorized(lambda: user2_session.execute(f'CREATE MATERIALIZED VIEW {mv} AS SELECT * FROM {table2} WHERE p IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, p)'))
|
|
eventually_authorized(lambda: user2_session.execute(f'SELECT * FROM {mv}'))
|
|
eventually_unauthorized(lambda: user1_session.execute(f'SELECT * FROM {mv}'))
|
|
eventually_authorized(lambda: user2_session.execute(f'DROP MATERIALIZED VIEW {mv}'))
|
|
|
|
# This is a scylla_only test because it tests the Scylla-only CDC feature.
|
|
def test_auto_revoke_cdc(cql, test_keyspace, scylla_only):
|
|
with new_user(cql) as user1, new_user(cql) as user2:
|
|
# Grant user1 and user2 CREATE permissions on test_keyspace, but no
|
|
# other permissions:
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, user1)
|
|
grant(cql, 'CREATE', 'KEYSPACE ' + test_keyspace, user2)
|
|
with new_session(cql, user1) as user1_session, new_session(cql, user2) as user2_session:
|
|
schema = "p int primary key"
|
|
extra = "with cdc = {'enabled': true}"
|
|
table = f'{test_keyspace}.{unique_name()}'
|
|
with eventually_new_named_table(user1_session, table, schema, extra):
|
|
pass
|
|
# At this point, table was deleted by user1, so user2 can create
|
|
# a table with the same name "table" (and a CDC log for it)
|
|
with eventually_new_named_table(user2_session, table, schema, extra):
|
|
# Now, permissions were auto-granted for user2, but user1
|
|
# no longer has permissions on this table.
|
|
eventually_authorized(lambda: user2_session.execute(f'SELECT * FROM {table}_scylla_cdc_log')) # Reproduces #19798
|
|
eventually_unauthorized(lambda: user1_session.execute(f'SELECT * FROM {table}_scylla_cdc_log'))
|
|
|
|
# This test confirms that it is not possible to set separate permissions on a
|
|
# materialized view - rather, the SELECT statement checks the permissions of
|
|
# the base table instead of those on the view table. To make a view readable
|
|
# you need to give read permissions on the base table. Consequently, it is
|
|
# not possible to make different views of the same base table have different
|
|
# permissions. Reproduces #25800.
|
|
def test_view_permissions_from_base(cql, test_keyspace):
|
|
with new_test_table(cql, test_keyspace, 'a int primary key') as table:
|
|
with new_materialized_view(cql, table, '*', 'a', 'a is not null') as mv:
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
# The new user will not have permissions to read either
|
|
# the base table or the view, since we haven't granted
|
|
# any permissions.
|
|
eventually_unauthorized(lambda: user_session.execute(f'SELECT * FROM {table}'), timeout_s=0)
|
|
eventually_unauthorized(lambda: user_session.execute(f'SELECT * FROM {mv}'), timeout_s=0)
|
|
# Try to grant SELECT permission on the
|
|
# *materialized view*. This operation is allowed
|
|
# (no error thrown, currently), but has NO EFFECT:
|
|
# the view is still unreadable for the user!
|
|
grant(cql, 'SELECT', mv, username)
|
|
ensure_updated_permissions(cql)
|
|
eventually_unauthorized(lambda: user_session.execute(f'SELECT * FROM {mv}'), timeout_s=0)
|
|
# Turns out that to make the view readable, we
|
|
# must grant SELECT permissions on the base table.
|
|
# If we do that, the view becomes readable:
|
|
grant(cql, 'SELECT', table, username)
|
|
eventually_authorized(lambda: user_session.execute(f'SELECT * FROM {mv}'))
|
|
|
|
# This test confirms that it is not possible to set separate permissions on a
|
|
# CDC log - rather, the SELECT statement checks the permissions of the base
|
|
# table instead of those on the CDC log table. To make a CDC log readable
|
|
# you need to give read permissions on the base table.
|
|
# Reproduces #19798 and #25800.
|
|
def test_cdc_permissions_from_base(cql, test_keyspace, scylla_only):
|
|
with new_test_table(cql, test_keyspace, 'a int primary key', "with cdc={'enabled':true}") as table:
|
|
cdc_log = table + '_scylla_cdc_log'
|
|
with new_user(cql) as username:
|
|
with new_session(cql, username) as user_session:
|
|
# The new user does not have permissions to read either the
|
|
# base table or the CDC log, since we haven't granted it any
|
|
# permissions.
|
|
eventually_unauthorized(lambda: user_session.execute(f'SELECT * FROM {table}'), timeout_s=0)
|
|
eventually_unauthorized(lambda: user_session.execute(f'SELECT * FROM {cdc_log}'), timeout_s=0)
|
|
# Try to grant SELECT permission on the CDC log table. This
|
|
# operation is allowed (no error thrown, currently), but has
|
|
# no effect: the CDC log is still unreadable for the user!
|
|
grant(cql, 'SELECT', cdc_log, username)
|
|
ensure_updated_permissions(cql)
|
|
eventually_unauthorized(lambda: user_session.execute(f'SELECT * FROM {cdc_log}'), timeout_s=0)
|
|
# To make the CDC log really readable, we must grant SELECT
|
|
# permissions on the *base* table. If we do that, the CDC log
|
|
# becomes readable:
|
|
grant(cql, 'SELECT', table, username)
|
|
eventually_authorized(lambda: user_session.execute(f'SELECT * FROM {cdc_log}'))
|
|
# Of course the base table became readable too:
|
|
eventually_authorized(lambda: user_session.execute(f'SELECT * FROM {table}'))
|
|
|
|
# Test that an unprivileged user can read from *some* system tables, such
|
|
# as system_schema.tables, but cannot read from *other* system tables - most
|
|
# notably the system.roles table. Allowing unprivileged users to read from
|
|
# the roles table could have allowed them to read other users' salted hash
|
|
# keys (which can be used to brute-force the other users' passwords, and in
|
|
# Alternator - to directly log as those users!)
|
|
def test_select_system_table(cql):
|
|
roles_table = get_roles_table(cql)
|
|
|
|
with new_user(cql) as user1:
|
|
with new_session(cql, user1) as user1_session:
|
|
# user1 was not explicitly granted permissions for
|
|
# system_schema.tables, but can still read it!
|
|
# This white-listing of certain system tables that even an
|
|
# unprivileged user can read happens in the function
|
|
# service::client_state::has_access().
|
|
eventually_authorized(lambda: user1_session.execute('SELECT * FROM system_schema.tables'))
|
|
# But the system.roles table is NOT white-listed - it should be
|
|
# unreadable to the unprivileged user, until explicitly granted
|
|
# the permission to read it:
|
|
eventually_unauthorized(lambda: user1_session.execute(f'SELECT * FROM {roles_table}'))
|
|
grant(cql, 'SELECT', roles_table, user1)
|
|
eventually_authorized(lambda: user1_session.execute(f'SELECT * FROM {roles_table}'))
|