Skip to content

Commit f4f7666

Browse files
dirkkultsmith023
andauthored
Add support for groups (#1778)
* Add support for groups * Add missing output types * Fix port for group test * Fix concurrent integration tests * Correct path * Add groups to own user * Escape group names * Also escape user_ids * Fix integration test * Update CI image * Remove unnecessary inheritance of `_Groups` on `_BaseExecutor` --------- Co-authored-by: Tommy Smith <tommy@weaviate.io>
1 parent b19e309 commit f4f7666

20 files changed

Lines changed: 559 additions & 11 deletions

File tree

.github/workflows/main.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ env:
2525
WEAVIATE_129: 1.29.9
2626
WEAVIATE_130: 1.30.12
2727
WEAVIATE_131: 1.31.5
28-
WEAVIATE_132: 1.32.4-cdf9a3b
28+
WEAVIATE_132: 1.32.5
2929

3030
jobs:
3131
lint-and-format:

integration/test_groups.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import pytest
2+
3+
from integration.conftest import ClientFactory
4+
from weaviate.auth import Auth
5+
from weaviate.rbac.models import GroupTypes, Role
6+
7+
RBAC_PORTS = (8092, 50063)
8+
RBAC_AUTH_CREDS = Auth.api_key("admin-key")
9+
10+
11+
def test_assign_and_get_group_roles_oidc(client_factory: ClientFactory) -> None:
12+
with client_factory(ports=RBAC_PORTS, auth_credentials=RBAC_AUTH_CREDS) as client:
13+
if client._connection._weaviate_version.is_lower_than(1, 32, 0):
14+
pytest.skip("This test requires Weaviate 1.32.0 or higher")
15+
16+
roles_to_assign = ["viewer", "admin"]
17+
group_id = "/assign-group"
18+
client.groups.oidc.revoke_roles(group_id=group_id, role_names=roles_to_assign)
19+
20+
roles_base = client.groups.oidc.get_assigned_roles(
21+
group_id=group_id,
22+
)
23+
assert len(roles_base) == 0
24+
25+
client.groups.oidc.assign_roles(group_id=group_id, role_names=roles_to_assign)
26+
27+
roles = client.groups.oidc.get_assigned_roles(group_id=group_id, include_permissions=True)
28+
assert len(roles) == 2
29+
for role_name in roles_to_assign:
30+
assert role_name in roles
31+
assert isinstance(roles[role_name], Role)
32+
33+
client.groups.oidc.revoke_roles(group_id=group_id, role_names=roles_to_assign)
34+
35+
roles_base = client.groups.oidc.get_assigned_roles(
36+
group_id=group_id,
37+
)
38+
assert len(roles_base) == 0
39+
40+
41+
def test_known_groups(client_factory: ClientFactory) -> None:
42+
with client_factory(ports=RBAC_PORTS, auth_credentials=RBAC_AUTH_CREDS) as client:
43+
if client._connection._weaviate_version.is_lower_than(1, 32, 0):
44+
pytest.skip("This test requires Weaviate 1.32.0 or higher")
45+
46+
group1 = "/known-group1"
47+
group2 = "/known-group2"
48+
client.groups.oidc.assign_roles(group_id=group1, role_names="viewer")
49+
client.groups.oidc.assign_roles(group_id=group2, role_names="viewer")
50+
51+
groups = client.groups.oidc.get_known_group_names()
52+
assert len(groups) >= 2 # other tests may add groups
53+
assert group1 in groups
54+
assert group2 in groups
55+
56+
client.groups.oidc.revoke_roles(group_id=group1, role_names="viewer")
57+
client.groups.oidc.revoke_roles(group_id=group2, role_names="viewer")
58+
59+
groups = client.groups.oidc.get_known_group_names()
60+
assert len(groups) >= 0 # other tests may add groups
61+
assert group1 not in groups
62+
assert group2 not in groups
63+
64+
65+
def test_get_group_assignments(client_factory: ClientFactory) -> None:
66+
with client_factory(ports=RBAC_PORTS, auth_credentials=RBAC_AUTH_CREDS) as client:
67+
if client._connection._weaviate_version.is_lower_than(1, 32, 0):
68+
pytest.skip("This test requires Weaviate 1.32.0 or higher")
69+
70+
role_name = "test_group_assignments_role"
71+
client.roles.delete(role_name=role_name)
72+
client.roles.create(role_name=role_name, permissions=[])
73+
74+
group_assignments = client.roles.get_group_assignments(role_name=role_name)
75+
assert len(group_assignments) == 0
76+
77+
client.groups.oidc.assign_roles(group_id="custom-group", role_names=role_name)
78+
client.groups.oidc.assign_roles(group_id="custom-group2", role_names=role_name)
79+
80+
group_assignments = client.roles.get_group_assignments(role_name=role_name)
81+
assert len(group_assignments) == 2
82+
for group in group_assignments:
83+
assert group.group_id in ["custom-group", "custom-group2"]
84+
assert group.group_type == GroupTypes.OIDC
85+
86+
client.groups.oidc.revoke_roles(group_id="custom-group", role_names=role_name)
87+
client.groups.oidc.revoke_roles(group_id="custom-group2", role_names=role_name)

integration/test_rbac.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ClusterPermissionOutput,
1313
CollectionsPermissionOutput,
1414
DataPermissionOutput,
15+
GroupsPermissionOutput,
1516
NodesPermissionOutput,
1617
Role,
1718
ReplicatePermissionOutput,
@@ -45,6 +46,7 @@
4546
nodes_permissions=[],
4647
tenants_permissions=[],
4748
replicate_permissions=[],
49+
groups_permissions=[],
4850
),
4951
None,
5052
),
@@ -62,6 +64,7 @@
6264
nodes_permissions=[],
6365
tenants_permissions=[],
6466
replicate_permissions=[],
67+
groups_permissions=[],
6568
),
6669
None,
6770
),
@@ -83,6 +86,7 @@
8386
nodes_permissions=[],
8487
tenants_permissions=[],
8588
replicate_permissions=[],
89+
groups_permissions=[],
8690
),
8791
None,
8892
),
@@ -102,6 +106,7 @@
102106
nodes_permissions=[],
103107
tenants_permissions=[],
104108
replicate_permissions=[],
109+
groups_permissions=[],
105110
),
106111
None,
107112
),
@@ -134,6 +139,7 @@
134139
nodes_permissions=[],
135140
tenants_permissions=[],
136141
replicate_permissions=[],
142+
groups_permissions=[],
137143
),
138144
None,
139145
),
@@ -155,6 +161,7 @@
155161
],
156162
tenants_permissions=[],
157163
replicate_permissions=[],
164+
groups_permissions=[],
158165
),
159166
None,
160167
),
@@ -176,6 +183,7 @@
176183
],
177184
tenants_permissions=[],
178185
replicate_permissions=[],
186+
groups_permissions=[],
179187
),
180188
None,
181189
),
@@ -197,6 +205,7 @@
197205
nodes_permissions=[],
198206
tenants_permissions=[],
199207
replicate_permissions=[],
208+
groups_permissions=[],
200209
),
201210
None,
202211
),
@@ -220,6 +229,7 @@
220229
)
221230
],
222231
replicate_permissions=[],
232+
groups_permissions=[],
223233
),
224234
None,
225235
),
@@ -260,6 +270,7 @@
260270
),
261271
],
262272
replicate_permissions=[],
273+
groups_permissions=[],
263274
),
264275
None,
265276
),
@@ -281,6 +292,7 @@
281292
nodes_permissions=[],
282293
tenants_permissions=[],
283294
replicate_permissions=[],
295+
groups_permissions=[],
284296
),
285297
None,
286298
),
@@ -321,6 +333,7 @@
321333
actions={Actions.Replicate.READ, Actions.Replicate.UPDATE},
322334
),
323335
],
336+
groups_permissions=[],
324337
),
325338
32,
326339
),
@@ -344,6 +357,7 @@
344357
nodes_permissions=[],
345358
tenants_permissions=[],
346359
replicate_permissions=[],
360+
groups_permissions=[],
347361
),
348362
32, # Minimum version for alias permissions
349363
),
@@ -367,9 +381,34 @@
367381
nodes_permissions=[],
368382
tenants_permissions=[],
369383
replicate_permissions=[],
384+
groups_permissions=[],
370385
),
371386
32, # Minimum version for alias permissions
372387
),
388+
(
389+
Permissions.Groups.oidc(group="MyGroup", read=True),
390+
Role(
391+
name="GroupRole",
392+
alias_permissions=[],
393+
cluster_permissions=[],
394+
users_permissions=[],
395+
collections_permissions=[],
396+
roles_permissions=[],
397+
data_permissions=[],
398+
backups_permissions=[],
399+
nodes_permissions=[],
400+
tenants_permissions=[],
401+
replicate_permissions=[],
402+
groups_permissions=[
403+
GroupsPermissionOutput(
404+
group="MyGroup",
405+
group_type="oidc",
406+
actions={Actions.Groups.READ},
407+
)
408+
],
409+
),
410+
32, # Minimum version for group permissions
411+
),
373412
],
374413
)
375414
def test_create_role(

integration/test_users.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ def test_create_user_and_get(client_factory: ClientFactory) -> None:
8787
) as client2:
8888
user = client2.users.get_my_user()
8989
assert user.user_id == randomUserName
90-
user = client.users.db.get(user_id=randomUserName)
91-
assert user.user_id == randomUserName
92-
assert user.user_type == UserTypes.DB_DYNAMIC
90+
dynamicUser = client.users.db.get(user_id=randomUserName)
91+
assert dynamicUser is not None
92+
assert dynamicUser.user_id == randomUserName
93+
assert dynamicUser.user_type == UserTypes.DB_DYNAMIC
9394
assert client.users.db.delete(user_id=randomUserName)
9495

9596

weaviate/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .connect.v4 import ConnectionAsync, ConnectionSync
2121
from .debug import _Debug, _DebugAsync
2222
from .embedded import EmbeddedOptions
23+
from .groups import _Groups, _GroupsAsync
2324
from .rbac import _Roles, _RolesAsync
2425
from .types import NUMBER
2526
from .users import _Users, _UsersAsync
@@ -78,6 +79,7 @@ def __init__(
7879
self.cluster = _ClusterAsync(self._connection)
7980
self.collections = _CollectionsAsync(self._connection)
8081
self.debug = _DebugAsync(self._connection)
82+
self.groups = _GroupsAsync(self._connection)
8183
self.roles = _RolesAsync(self._connection)
8284
self.users = _UsersAsync(self._connection)
8385

@@ -148,6 +150,7 @@ def __init__(
148150
self.cluster = _Cluster(self._connection)
149151
self.collections = collections
150152
self.debug = _Debug(self._connection)
153+
self.groups = _Groups(self._connection)
151154
self.roles = _Roles(self._connection)
152155
self.users = _Users(self._connection)
153156

weaviate/client.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ from weaviate.collections.classes.internal import _RawGQLReturn
1111
from weaviate.collections.collections.async_ import _CollectionsAsync
1212
from weaviate.collections.collections.sync import _Collections
1313
from weaviate.connect.v4 import ConnectionAsync, ConnectionSync
14+
from weaviate.groups.async_ import _GroupsAsync
15+
from weaviate.groups.sync import _Groups
1416
from weaviate.users.async_ import _UsersAsync
1517
from weaviate.users.sync import _Users
1618

@@ -30,6 +32,7 @@ class WeaviateAsyncClient(_WeaviateClientExecutor[ConnectionAsync]):
3032
collections: _CollectionsAsync
3133
cluster: _ClusterAsync
3234
debug: _DebugAsync
35+
groups: _GroupsAsync
3336
roles: _RolesAsync
3437
users: _UsersAsync
3538

@@ -52,6 +55,7 @@ class WeaviateClient(_WeaviateClientExecutor[ConnectionSync]):
5255
collections: _Collections
5356
cluster: _Cluster
5457
debug: _Debug
58+
groups: _Groups
5559
roles: _Roles
5660
users: _Users
5761

weaviate/groups/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .async_ import _GroupsAsync
2+
from .sync import _Groups
3+
4+
__all__ = ["_Groups", "_GroupsAsync"]

weaviate/groups/async_.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from weaviate.connect import executor
2+
from weaviate.connect.v4 import ConnectionAsync
3+
from weaviate.groups.base import _GroupsOIDCExecutor
4+
5+
6+
@executor.wrap("async")
7+
class _GroupsAsync:
8+
def __init__(self, connection: ConnectionAsync):
9+
self.oidc = _GroupsOIDCAsync(connection)
10+
11+
12+
@executor.wrap("sync")
13+
class _GroupsOIDCAsync(_GroupsOIDCExecutor[ConnectionAsync]):
14+
pass

weaviate/groups/async_.pyi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import Dict, List, Literal, Union, overload
2+
3+
from weaviate.connect.v4 import ConnectionAsync
4+
from weaviate.groups.base import _GroupsOIDCExecutor
5+
from weaviate.rbac.models import Role, RoleBase
6+
7+
class _GroupsOIDCAsync(_GroupsOIDCExecutor[ConnectionAsync]):
8+
@overload
9+
async def get_assigned_roles(
10+
self, *, group_id: str, include_permissions: Literal[False] = False
11+
) -> Dict[str, RoleBase]: ...
12+
@overload
13+
async def get_assigned_roles(
14+
self, *, group_id: str, include_permissions: Literal[True]
15+
) -> Dict[str, Role]: ...
16+
@overload
17+
async def get_assigned_roles(
18+
self, *, group_id: str, include_permissions: bool = False
19+
) -> Union[Dict[str, Role], Dict[str, RoleBase]]: ...
20+
async def assign_roles(self, *, group_id: str, role_names: Union[str, List[str]]) -> None: ...
21+
async def revoke_roles(self, *, group_id: str, role_names: Union[str, List[str]]) -> None: ...
22+
async def get_known_group_names(self) -> List[str]: ...
23+
24+
class _GroupsAsync:
25+
def __init__(self, connection: ConnectionAsync): ...
26+
@property
27+
def oidc(self) -> _GroupsOIDCAsync: ...

0 commit comments

Comments
 (0)