Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
joocer committed Dec 21, 2024
1 parent ae035aa commit a00f1f3
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 32 deletions.
21 changes: 19 additions & 2 deletions opteryx/managers/permissions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,33 @@ def load_permissions() -> List[Dict]:

def can_read_table(roles: Iterable[str], table: str, action: str = "READ") -> bool:
"""
Check if any of the provided roles have READ permission for the specified table.
Check if any of the provided roles have READ access to the specified table.
When we call this function, we provide the current user's roles and the table name.
We then check if any of the permissions in the system match those roles and if those permissions
grant access to the table.
Tables can have wildcards in their names, so we use fnmatch to check the table name.
We have a default role 'opteryx' with READ access to all tables.
Parameters:
roles (List[str]): A list of roles to check against permissions.
table (str): The name of the table to check access for.
Returns:
bool: True if any role has READ permission for the table, False otherwise.
bool: True if any role has READ access to the table, False otherwise.
"""

def escape_special_chars(pattern: str) -> str:
return pattern.replace(r"\*", "*").replace(r"\?", "?")

# If no permissions are loaded, default to allowing all reads.
if not PERMISSIONS:
return True

table = escape_special_chars(table)

for entry in PERMISSIONS:
# Check if the permission, the role is in the provided roles,
# and the table matches the pattern defined in the permission.
Expand All @@ -58,6 +72,9 @@ def can_read_table(roles: Iterable[str], table: str, action: str = "READ") -> bo
and entry["role"] in roles
and fnmatch.fnmatch(table, entry["table"])
):
# Additional check for leading dots
if table.startswith(".") and not entry["table"].startswith("."):
continue
return True

# If no matching permission is found, deny access.
Expand Down
13 changes: 12 additions & 1 deletion tests/misc/test_utils_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,22 @@
("bucket/path/path/path/path/path/file.ext", ("bucket", "path/path/path/path/path", "file", ".ext"), None),
("bucket/path/file.ext", ("bucket", "path", "file", ".ext"), None),
("bucket.ext/path.ext/file.ext", ("bucket.ext", "path.ext", "file", ".ext"), None),
# can't traverse up the folder structure
("../../path/file.ext", None, ValueError),
("path/../../path/file.ext", None, ValueError),
("~/path/file.ext", None, ValueError),
("~/file.ext", None, ValueError),
("/absolute/path/to/file.ext", ("", "absolute/path/to", "file", ".ext"), None),
("relative/path/to/file.ext", ("relative", "path/to", "file", ".ext"), None),
("./relative/path/to/file.ext", (".", "relative/path/to", "file", ".ext"), None),
("../relative/path/to/file.ext", None, ValueError),
("C:\\users\\opteryx\\file.ext", ("", "", "C:\\users\\opteryx\\file", ".ext"), None),
("bucket/path.with.dots/file.ext", ("bucket", "path.with.dots", "file", ".ext"), None),
("bucket/path with spaces/file.ext", ("bucket", "path with spaces", "file", ".ext"), None),
("bucket/path_with_underscores/file.ext", ("bucket", "path_with_underscores", "file", ".ext"), None),
("bucket/path-with-hyphens/file.ext", ("bucket", "path-with-hyphens", "file", ".ext"), None),
("bucket/path123/file.ext", ("bucket", "path123", "file", ".ext"), None),
("bucket/123path/file.ext", ("bucket", "123path", "file", ".ext"), None),

]
# fmt:on

Expand Down
54 changes: 33 additions & 21 deletions tests/security/test_execute_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,46 @@ def test_security_permissions_cursor():
curr.execute("EXPLAIN SELECT * FROM $planets")
curr.arrow()

def test_security_permissions_invalid():
"""test edge cases for permissions"""
# empty permissions set
with pytest.raises(opteryx.exceptions.PermissionsError):
opteryx.query("SELECT * FROM $planets", permissions=set()).arrow()

# permissions with invalid type
with pytest.raises(opteryx.exceptions.ProgrammingError):
opteryx.query("SELECT * FROM $planets", permissions={"InvalidPermission"}).arrow()

# permissions with mixed valid and invalid types
with pytest.raises(opteryx.exceptions.ProgrammingError):
opteryx.query("SELECT * FROM $planets", permissions={"Query", "InvalidPermission"}).arrow()

# permissions with empty string
with pytest.raises(opteryx.exceptions.ProgrammingError):
opteryx.query("SELECT * FROM $planets", permissions="").arrow()

# permissions with numeric values
with pytest.raises(opteryx.exceptions.ProgrammingError):
opteryx.query("SELECT * FROM $planets", permissions={1, 2, 3}).arrow()

# permissions with boolean values
with pytest.raises(opteryx.exceptions.ProgrammingError):
opteryx.query("SELECT * FROM $planets", permissions={True, False}).arrow()

# permissions with mixed valid and invalid types in a list
with pytest.raises(opteryx.exceptions.ProgrammingError):
opteryx.query("SELECT * FROM $planets", permissions=["Query", 123, None]).arrow()



def test_security_permissions_query():
"""test we can stop users performing some query types"""
# shouldn't have any issues
opteryx.query("EXPLAIN SELECT * FROM $planets").arrow()
# shouldn't have any issues
opteryx.query("SELECT * FROM $planets").arrow()
# None is equivalent to all permissions
opteryx.query("SELECT * FROM $planets", permissions=None).arrow()

# shouldn't have any issues
opteryx.query("SELECT * FROM $planets", permissions={"Query"}).arrow()
Expand All @@ -59,28 +92,7 @@ def test_security_permissions_validation():
opteryx.query("SELECT * FROM $planets", permissions={"Analyze", "Execute", "Query"}).arrow()
opteryx.query("SELECT * FROM $planets", permissions=["Analyze", "Execute", "Query"]).arrow()
opteryx.query("SELECT * FROM $planets", permissions=("Analyze", "Execute", "Query")).arrow()
# should fail
with pytest.raises(opteryx.exceptions.ProgrammingError):
# invalid permission
opteryx.query("SELECT * FROM $planets", permissions={"Select"}).arrow()
with pytest.raises(opteryx.exceptions.ProgrammingError):
# no permissions
opteryx.query("SELECT * FROM $planets", permissions={}).arrow()
with pytest.raises(opteryx.exceptions.ProgrammingError):
# invalid permission
opteryx.query("SELECT * FROM $planets", permissions={"Query", "Select"}).arrow()


def test_security_permissions_invalid_values():
with pytest.raises(opteryx.exceptions.ProgrammingError):
# invalid permission
opteryx.query("SELECT * FROM $planets", permissions=[1]).arrow()
with pytest.raises(opteryx.exceptions.ProgrammingError):
# invalid permission
opteryx.query("SELECT * FROM $planets", memberships=[1]).arrow()
with pytest.raises(opteryx.exceptions.ProgrammingError):
# invalid permission
opteryx.query("SELECT * FROM $planets", user=1).arrow()


if __name__ == "__main__": # pragma: no cover
Expand Down
15 changes: 12 additions & 3 deletions tests/security/test_row_visibility_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@
("SELECT * FROM $planets p LEFT JOIN $satellites s ON p.id = s.planetId", {"$satellites": [("id", "Lt", 4)]}, (10, 28)),

("SELECT * FROM $planets p1 JOIN $planets p2 ON p1.id = p2.id", {"$planets": [("id", "Gt", 3)], "p2": [("name", "NotEq", "X")]}, (6, 40)),

("SELECT * FROM $planets WHERE id = 4", {"$planets": [("id", "Eq", 4)]}, (1, 20)),
("SELECT * FROM $planets WHERE name = 'Mars'", {"$planets": [("name", "Eq", "Mars")]}, (1, 20)),
("SELECT * FROM $planets WHERE name LIKE 'M%'", {"$planets": [("name", "Like", "M%")]}, (2, 20)),
("SELECT * FROM $planets WHERE id > 3 AND name LIKE 'M%'", {"$planets": [("id", "Gt", 3), ("name", "Like", "M%")]}, (1, 20)),
("SELECT * FROM $planets WHERE id < 4 OR name LIKE 'M%'", {"$planets": [("id", "Lt", 4), ("name", "Like", "M%")]}, (1, 20)),
("SELECT * FROM $planets WHERE id = 4 AND name = 'Mars'", {"$planets": [("id", "Eq", 4), ("name", "Eq", "Mars")]}, (1, 20)),
("SELECT * FROM $planets WHERE id = 4 OR name = 'Mars'", {"$planets": [("id", "Eq", 4), ("name", "Eq", "Mars")]}, (1, 20)),
("SELECT * FROM $planets WHERE id = 4 AND name LIKE 'M%'", {"$planets": [("id", "Eq", 4), ("name", "Like", "M%")]}, (1, 20)),
("SELECT * FROM $planets WHERE name LIKE 'M%'", {"$planets": [("id", "Eq", 4), ("name", "Like", "M%")]}, (1, 20)),
("SELECT * FROM $planets WHERE id = 4", {"$planets": [("id", "Eq", 4), ("name", "NotLike", "M%")]}, (0, 20)),
("SELECT * FROM $planets", {"$planets": [("id", "Eq", 4), ("name", "NotLike", "M%")]}, (0, 20)),
]


Expand Down Expand Up @@ -102,6 +114,3 @@ def test_visibility_filters(sql, filters, shape):
f" \033[38;2;26;185;67m{passed} passed ({(passed * 100) // (passed + failed)}%)\033[0m\n"
f" \033[38;2;255;121;198m{failed} failed\033[0m"
)



48 changes: 47 additions & 1 deletion tests/security/test_table_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,52 @@
(["restricted", "opteryx"], "db.schema.table", True),
(["restricted", "opteryx"], "opteryx.schema.deeply.nested.table", True),
(["restricted", "opteryx"], "other.schema.table", True),
(["opteryx"], "", True), # Empty table name
([], "", False), # Empty roles and table name
([""], "", False), # Empty role and table name
(["opteryx"], " ", True), # Table name with space
([" "], "opteryx.table1", False), # Role with space
(["opteryx"], "opteryx..table", True), # Table name with double dots
(["opteryx"], ".opteryx.table", False), # Table name starting with dot
(["opteryx"], "opteryx.table.", True), # Table name ending with dot
(["opteryx"], "opteryx..schema.table", True), # Table name with double dots in schema
(["opteryx"], "opteryx.schema..table", True), # Table name with double dots in table
(["opteryx"], "opteryx.schema.table..", True), # Table name ending with double dots
(["opteryx"], "opteryx.table_with_special_chars!@#$%^&*()", True), # Special characters in table name
(["opteryx"], "Opteryx.Table", True), # Mixed case table name
(["opteryx"], "opteryx." + "a" * 255, True), # Very long table name
(["table_with_special_chars!@#$%^&*()"], "opteryx.table1", False), # Role with special characters
(["table_with_special_chars!@#$%^&*()"], "opteryx.table1", False), # Role with special characters
(["table_with_special_chars!@#$%^&*()"], "opteryx.table_with_special_chars!@#$%^&*()", False), # Role and table with special characters
(["table_with_special_chars!@#$%^&*()"], "opteryx.table_with_underscore", False), # Role with special characters and table with underscore
(["table_with_special_chars!@#$%^&*()"], "opteryx.table-with-dash", False), # Role with special characters and table with dash
(["table_with_special_chars!@#$%^&*()"], "opteryx.table/with/slash", False), # Role with special characters and table with slash
(["table_with_special_chars!@#$%^&*()"], "opteryx.table\\with\\backslash", False), # Role with special characters and table with backslash
(["table_with_special_chars!@#$%^&*()"], "opteryx.table:with:colon", False), # Role with special characters and table with colon
(["table_with_special_chars!@#$%^&*()"], "opteryx.table;with:semicolon", False), # Role with special characters and table with semicolon
(["table_with_special_chars!@#$%^&*()"], "opteryx.table,with,comma", False), # Role with special characters and table with comma
(["table_with_special_chars!@#$%^&*()"], "opteryx.table<with<less<than", False), # Role with special characters and table with less than
(["table_with_special_chars!@#$%^&*()"], "opteryx.table>with>greater>than", False), # Role with special characters and table with greater than
(["table_with_special_chars!@#$%^&*()"], "opteryx.table|with|pipe", False), # Role with special characters and table with pipe
(["table_with_special_chars!@#$%^&*()"], "opteryx.table?with?question?mark", False), # Role with special characters and table with question mark
(["table_with_special_chars!@#$%^&*()"], "opteryx.table*with*asterisk", False), # Role with special characters and table with asterisk
(["table_with_special_chars!@#$%^&*()"], "opteryx.table\"with\"double\"quote", False), # Role with special characters and table with double quote
(["table_with_special_chars!@#$%^&*()"], "opteryx.table'with'single'quote", False), # Role with special characters and table with single quote
(["opteryx"], "opteryx.table_with_underscore", True), # Table name with underscore
(["opteryx"], "opteryx.table-with-dash", True), # Table name with dash
(["opteryx"], "opteryx.table/with/slash", True), # Table name with slash
(["opteryx"], "opteryx.table\\with\\backslash", True), # Table name with backslash
(["opteryx"], "opteryx.table:with:colon", True), # Table name with colon
(["opteryx"], "opteryx.table;with:semicolon", True), # Table name with semicolon
(["opteryx"], "opteryx.table,with,comma", True), # Table name with comma
(["opteryx"], "opteryx.table<with<less<than", True), # Table name with less than
(["opteryx"], "opteryx.table>with>greater>than", True), # Table name with greater than
(["opteryx"], "opteryx.table|with|pipe", True), # Table name with pipe
(["opteryx"], "opteryx.table?with?question?mark", True), # Table name with question mark
(["opteryx"], "opteryx.table*with*asterisk", True), # Table name with asterisk
(["opteryx"], "opteryx.table\"with\"double\"quote", True), # Table name with double quote
(["opteryx"], "opteryx.table'with'single'quote", True), # Table name with single quote

]

@pytest.mark.parametrize("roles, table, expected", test_cases)
Expand All @@ -69,7 +115,7 @@ def test_can_read_table(roles, table, expected):
for index, (roles, table, expected) in enumerate(test_cases):
print(
f"\033[38;2;255;184;108m{(index + 1):04}\033[0m"
f" .",
f" {', '.join(roles).ljust(35)} {table.ljust(25)}",
end="",
flush=True,
)
Expand Down
25 changes: 21 additions & 4 deletions tests/sql_battery/test_null_semantics.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,27 @@
"""
-- Query 24: Expected rows: 3 (1, -1, NULL)
SELECT * FROM (VALUES (1), (-1), (NULL)) AS tristatebooleans(bool) WHERE NOT bool IS NULL;
""", {1, -1})



""", {1, -1}),(
"""
-- Query 25: SELECT * FROM tristatebooleans WHERE bool IS NULL;
-- Expected rows: 1 (NULL)
SELECT * FROM (VALUES (True), (False), (NULL)) AS tristatebooleans(bool) WHERE bool IS NULL;
""", {None}),(
"""
-- Query 26: SELECT * FROM tristatebooleans WHERE bool IS NOT TRUE;
-- Expected rows: 2 (False, NULL)
SELECT * FROM (VALUES (True), (False), (NULL)) AS tristatebooleans(bool) WHERE bool IS NOT TRUE;
""", {False, None}),(
"""
-- Query 27: SELECT * FROM tristatebooleans WHERE bool IS NOT FALSE;
-- Expected rows: 2 (True, NULL)
SELECT * FROM (VALUES (True), (False), (NULL)) AS tristatebooleans(bool) WHERE bool IS NOT FALSE;
""", {True, None}),(
"""
-- Query 28: SELECT * FROM tristatebooleans WHERE (bool IS NULL AND bool IS NOT NULL) OR (bool IS NOT NULL AND bool IS NULL) OR (bool <> bool);
-- Expected rows: 1 (NULL)
SELECT * FROM (VALUES (True), (False), (NULL)) AS tristatebooleans(bool) WHERE (bool IS NULL AND bool IS NOT NULL) OR (bool IS NOT NULL AND bool IS NULL) OR (bool <> bool);
""", {None})
]
# fmt:on

Expand Down

0 comments on commit a00f1f3

Please sign in to comment.