Skip to content

Commit 6cc9c2f

Browse files
committed
Add Roles' method has_permissions.
This commit adds a new method has_permissions, which returns true or false if the permissions passed in the method's argument are present in the role. Otherwise it will return false.
1 parent a1deabb commit 6cc9c2f

File tree

2 files changed

+255
-3
lines changed

2 files changed

+255
-3
lines changed

integration/test_rbac.py

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from typing import Generator, List, Callable
12
import pytest
2-
3+
import weaviate
34
from integration.conftest import ClientFactory
45
from weaviate.auth import Auth
56
from weaviate.rbac.models import (
@@ -12,8 +13,10 @@
1213
NodesPermission,
1314
)
1415

15-
RBAC_PORTS = (8092, 50063)
16-
RBAC_AUTH_CREDS = Auth.api_key("existing-key")
16+
# RBAC_PORTS = (8092, 50063)
17+
# RBAC_AUTH_CREDS = Auth.api_key("existing-key")
18+
RBAC_PORTS = (8080, 50051)
19+
RBAC_AUTH_CREDS = Auth.api_key("admin-key")
1720

1821

1922
@pytest.mark.parametrize(
@@ -241,3 +244,182 @@ def test_downsert_permissions(client_factory: ClientFactory) -> None:
241244
assert role is None
242245
finally:
243246
client.roles.delete(role_name)
247+
248+
249+
@pytest.fixture
250+
def test_has_permissions_setup(client_factory: ClientFactory) -> Generator[Callable, None, None]:
251+
created_roles = set()
252+
253+
def _setup(role_name: str, permissions: List[RBAC.permissions]) -> weaviate.WeaviateClient:
254+
client = client_factory(ports=RBAC_PORTS, auth_credentials=RBAC_AUTH_CREDS).__enter__()
255+
if client._connection._weaviate_version.is_lower_than(1, 28, 0):
256+
pytest.skip("This test requires Weaviate 1.28.0 or higher")
257+
client.roles.create(name=role_name, permissions=permissions)
258+
created_roles.add(role_name)
259+
return client
260+
261+
yield _setup
262+
263+
# Cleanup after all tests
264+
client = client_factory(ports=RBAC_PORTS, auth_credentials=RBAC_AUTH_CREDS)
265+
try:
266+
for role_name in created_roles:
267+
try:
268+
client.roles.delete(role_name)
269+
except Exception as e:
270+
print(f"Warning: Failed to cleanup role {role_name}: {str(e)}")
271+
finally:
272+
client.close()
273+
274+
275+
@pytest.mark.parametrize(
276+
"role_name,permissions,expected",
277+
[
278+
# Data permissions test
279+
(
280+
"data_role",
281+
[
282+
RBAC.permissions.data.read(collection="TestCollection"),
283+
RBAC.permissions.data.update(collection="TestCollection"),
284+
],
285+
[
286+
(RBAC.permissions.data.read(collection="TestCollection"), True),
287+
(RBAC.permissions.data.update(collection="TestCollection"), True),
288+
(RBAC.permissions.data.delete(collection="TestCollection"), False),
289+
(RBAC.permissions.data.read(collection="OtherCollection"), False),
290+
],
291+
),
292+
# Config permissions test
293+
(
294+
"config_role",
295+
[
296+
RBAC.permissions.config.update(collection="TestCollection"),
297+
],
298+
[
299+
(RBAC.permissions.config.update(collection="TestCollection"), True),
300+
(RBAC.permissions.config.update(collection="OtherCollection"), False),
301+
],
302+
),
303+
# Cluster permissions test
304+
(
305+
"cluster_role",
306+
[
307+
RBAC.permissions.cluster.read(),
308+
],
309+
[
310+
(RBAC.permissions.cluster.read(), True),
311+
(RBAC.permissions.config.update(collection="OtherCollection"), False),
312+
],
313+
),
314+
# Nodes permissions test
315+
(
316+
"nodes_role",
317+
[
318+
RBAC.permissions.nodes.read(collection="TestCollection", verbosity="verbose"),
319+
],
320+
[
321+
(
322+
RBAC.permissions.nodes.read(collection="TestCollection", verbosity="verbose"),
323+
True,
324+
),
325+
(
326+
RBAC.permissions.nodes.read(collection="OtherCollection", verbosity="minimal"),
327+
False,
328+
),
329+
(
330+
RBAC.permissions.nodes.read(collection="TestCollection", verbosity="minimal"),
331+
False,
332+
),
333+
],
334+
),
335+
# Users permissions test with wildcard
336+
(
337+
"users_role",
338+
[
339+
RBAC.permissions.users.manage(user="*"),
340+
],
341+
[
342+
(RBAC.permissions.users.manage(user="*"), True),
343+
(RBAC.permissions.users.manage(user="specific_user"), False),
344+
],
345+
),
346+
# Roles permissions test
347+
(
348+
"roles_role",
349+
[
350+
RBAC.permissions.roles.manage(role="newrole"),
351+
],
352+
[
353+
(RBAC.permissions.roles.manage(role="newrole"), True),
354+
(RBAC.permissions.roles.manage(role="otherrole"), False),
355+
],
356+
),
357+
# Backups permissions test
358+
(
359+
"backups_role",
360+
[
361+
RBAC.permissions.backups.manage(collection="testcollection"),
362+
],
363+
[
364+
(RBAC.permissions.backups.manage(collection="testcollection"), True),
365+
(RBAC.permissions.backups.manage(collection="othercollection"), False),
366+
],
367+
),
368+
# Multiple permission types test
369+
(
370+
"mixed_role",
371+
[
372+
RBAC.permissions.data.read(collection="TestCollection"),
373+
RBAC.permissions.config.update(collection="TestCollection"),
374+
RBAC.permissions.cluster.read(),
375+
RBAC.permissions.nodes.read(collection="TestCollection", verbosity="verbose"),
376+
],
377+
[
378+
(RBAC.permissions.data.read(collection="TestCollection"), True),
379+
(RBAC.permissions.config.update(collection="TestCollection"), True),
380+
(RBAC.permissions.cluster.read(), True),
381+
(
382+
RBAC.permissions.nodes.read(collection="TestCollection", verbosity="verbose"),
383+
True,
384+
),
385+
(RBAC.permissions.nodes.read(verbosity="verbose"), False),
386+
(RBAC.permissions.data.update(collection="TestCollection"), False),
387+
(
388+
[
389+
RBAC.permissions.data.read(collection="TestCollection"),
390+
RBAC.permissions.config.update(collection="TestCollection"),
391+
RBAC.permissions.cluster.read(),
392+
],
393+
True,
394+
),
395+
(
396+
[
397+
RBAC.permissions.data.read(collection="TestCollection"),
398+
RBAC.permissions.data.delete(collection="TestCollection"),
399+
],
400+
False,
401+
),
402+
],
403+
),
404+
],
405+
)
406+
def test_has_permissions(
407+
test_has_permissions_setup: weaviate.WeaviateClient, role_name, permissions, expected
408+
):
409+
"""Test has_permissions with different permission combinations.
410+
411+
Args:
412+
client: The Weaviate client
413+
role_name: Name of the role to create
414+
permissions: List of permissions to assign to the role
415+
expected: List of (permission, expected_result) tuples to test
416+
"""
417+
# Create role with permissions
418+
client = test_has_permissions_setup(role_name, permissions)
419+
# Test each permission combination
420+
for permission, expected_result in expected:
421+
result = client.roles.has_permissions(
422+
permissions=permission,
423+
role=role_name,
424+
)
425+
assert result == expected_result, f"Permission check failed for {permission}"

weaviate/rbac/roles.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
User,
1111
WeaviatePermission,
1212
WeaviateRole,
13+
ClusterAction,
14+
UsersAction,
15+
ConfigAction,
16+
RolesAction,
17+
DataAction,
18+
BackupsAction,
19+
NodesAction,
1320
)
1421

1522

@@ -256,3 +263,66 @@ async def remove_permissions(self, *, permissions: Permissions, role: str) -> No
256263
await self._remove_permissions(
257264
[permission._to_weaviate() for permission in permissions], role
258265
)
266+
267+
async def has_permissions(self, *, permissions: Permissions, role: str) -> bool:
268+
"""Check if a role has specific permissions.
269+
270+
Args:
271+
permissions: The permissions to check for.
272+
role: The role to check the permissions of.
273+
274+
Returns:
275+
True if the role has all the specified permissions, False otherwise.
276+
"""
277+
if isinstance(permissions, _Permission):
278+
permissions = [permissions]
279+
280+
role_obj = await self._get_role(role)
281+
if role_obj is None:
282+
return False
283+
284+
def normalize_permission(perm: WeaviatePermission) -> dict:
285+
"""Extract only the relevant fields for comparison based on action type."""
286+
action = perm["action"]
287+
288+
if action in ClusterAction.values():
289+
return {"action": action}
290+
elif action in UsersAction.values():
291+
return {"action": action, "user": perm["user"]}
292+
elif action in ConfigAction.values():
293+
return {
294+
"action": action,
295+
"collection": perm["collection"],
296+
"tenant": perm.get("tenant", "*"),
297+
}
298+
elif action in RolesAction.values():
299+
return {"action": action, "role": perm["role"]}
300+
elif action in DataAction.values():
301+
return {"action": action, "collection": perm["collection"]}
302+
elif action in BackupsAction.values():
303+
return {"action": action, "backup": {"collection": perm["backup"]["collection"]}}
304+
elif action in NodesAction.values():
305+
return {
306+
"action": action,
307+
"nodes": {
308+
"collection": perm["nodes"].get("collection", "*"),
309+
"verbosity": perm["nodes"]["verbosity"],
310+
},
311+
}
312+
# Default case: return a dict with all fields from the permission
313+
return dict(perm)
314+
315+
# Convert input permissions to normalized format
316+
check_perms = {
317+
json.dumps(normalize_permission(perm._to_weaviate()), sort_keys=True)
318+
for perm in permissions
319+
}
320+
321+
# Convert role permissions to normalized format
322+
role_perms = {
323+
json.dumps(normalize_permission(perm), sort_keys=True)
324+
for perm in role_obj["permissions"]
325+
}
326+
327+
# Check if all normalized input permissions exist in normalized role permissions
328+
return check_perms.issubset(role_perms)

0 commit comments

Comments
 (0)