Skip to content

Commit

Permalink
Merge pull request #1411 from weaviate/rbac_adapt_actions
Browse files Browse the repository at this point in the history
Move *_collection actions to Collection
  • Loading branch information
tsmith023 authored Nov 14, 2024
2 parents 8a7b873 + 506284f commit 69b24bc
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ env:
WEAVIATE_125: 1.25.24
WEAVIATE_126: 1.26.8
WEAVIATE_127: 1.27.1
WEAVIATE_128: preview-adapt-to-permissions-schema-changes-d40578e
WEAVIATE_128: 1.28.0-dev-396bf04

jobs:
lint-and-format:
Expand Down
6 changes: 3 additions & 3 deletions ci/docker-compose-rbac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ services:
environment:
ENABLE_MODULES: "generative-dummy,reranker-dummy"
AUTHENTICATION_APIKEY_ENABLED: "true"
AUTHENTICATION_APIKEY_ALLOWED_KEYS: "jane-secret-key,ian-secret-key,jp-secret-key"
AUTHENTICATION_APIKEY_USERS: "[email protected],ian-smith,jp-hwang"
AUTHENTICATION_APIKEY_ROLES: "viewer,editor,admin"
AUTHENTICATION_APIKEY_ALLOWED_KEYS: "existing-key"
AUTHENTICATION_APIKEY_USERS: "existing-user"
AUTHENTICATION_APIKEY_ROLES: "admin"
PERSISTENCE_DATA_PATH: "./data-weaviate-0"
CLUSTER_IN_LOCALHOST: "true"
CLUSTER_GOSSIP_BIND_PORT: "7100"
Expand Down
79 changes: 64 additions & 15 deletions integration/test_rbac.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,71 @@
import pytest

from integration.conftest import ClientFactory
from weaviate.auth import Auth
from weaviate.rbac.models import RBAC
from weaviate.rbac.models import (
RBAC,
Role,
CollectionsPermission,
RolesPermission,
TenantsPermission,
)


RBAC_PORTS = (8092, 50063)
RBAC_AUTH_CREDS = Auth.api_key("existing-key")


def test_create_role(client_factory: ClientFactory) -> None:
with client_factory(
ports=(8092, 50063), auth_credentials=Auth.api_key("jp-secret-key")
) as client:
@pytest.mark.parametrize(
"permissions,expected",
[
(
RBAC.permissions.collections(actions=RBAC.actions.collection.CREATE),
Role(
name="CreateAllCollections",
cluster_actions=None,
collections_permissions=[
CollectionsPermission(collection="*", action=RBAC.actions.collection.CREATE)
],
roles_permissions=None,
tenants_permissions=None,
),
),
(
RBAC.permissions.roles(actions=RBAC.actions.roles.MANAGE),
Role(
name="ManageAllRoles",
cluster_actions=None,
collections_permissions=None,
roles_permissions=[RolesPermission(role="*", action=RBAC.actions.roles.MANAGE)],
tenants_permissions=None,
),
),
(
RBAC.permissions.tenants(collection="foo", actions=RBAC.actions.tenants.READ),
Role(
name="ReadAllTenantsInFoo",
cluster_actions=None,
collections_permissions=None,
roles_permissions=None,
tenants_permissions=[
TenantsPermission(
collection="foo", tenant="*", action=RBAC.actions.tenants.READ
)
],
),
),
],
)
def test_create_role(client_factory: ClientFactory, permissions, expected) -> None:
with client_factory(ports=RBAC_PORTS, auth_credentials=RBAC_AUTH_CREDS) as client:
if client._connection._weaviate_version.is_lower_than(1, 28, 0):
pytest.skip("This test requires Weaviate 1.28.0 or higher")
client.roles.create(
name="CollectionCreator",
permissions=RBAC.permissions.database(actions=RBAC.actions.database.CREATE_COLLECTIONS),
)
role = client.roles.by_name("CollectionCreator")
assert role is not None
assert role.name == "CollectionCreator"
assert role.database_permissions is not None
assert len(role.database_permissions) == 1
assert role.database_permissions[0] == RBAC.actions.database.CREATE_COLLECTIONS
try:
client.roles.create(
name=expected.name,
permissions=permissions,
)
role = client.roles.by_name(expected.name)
assert role == expected
finally:
client.roles.delete(expected.name)
111 changes: 65 additions & 46 deletions weaviate/rbac/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,45 +24,42 @@ class _Action:


class TenantsAction(str, _Action, Enum):
CREATE_OBJECTS = "create_objects_tenant"
READ_OBJECTS = "read_objects_tenant"
UPDATE_OBJECTS = "update_objects_tenant"
DELETE_OBJECTS = "delete_objects_tenant"
CREATE = "create_tenants"
READ = "read_tenants"
UPDATE = "update_tenants"
DELETE = "delete_tenants"

@staticmethod
def values() -> List[str]:
return [action.value for action in TenantsAction]


class CollectionsAction(str, _Action, Enum):
CREATE_OBJECTS = "create_objects_collection"
READ_OBJECTS = "read_objects_collection"
UPDATE_OBJECTS = "update_objects_collection"
DELETE_OBJECTS = "delete_objects_collection"

CREATE_TENANTS = "create_tenants"
READ_TENANTS = "read_tenants"
UPDATE_TENANTS = "update_tenants"
DELETE_TENANTS = "delete_tenants"
CREATE = "create_collections"
READ = "read_collections"
UPDATE = "update_collections"
DELETE = "delete_collections"

@staticmethod
def values() -> List[str]:
return [action.value for action in CollectionsAction]


class DatabaseAction(str, _Action, Enum):
CREATE_COLLECTIONS = "create_collections"
READ_COLLECTIONS = "read_collections"
UPDATE_COLLECTIONS = "update_collections"
DELETE_COLLECTIONS = "delete_collections"
class RolesAction(str, _Action, Enum):
MANAGE = "manage_roles"
READ = "read_roles"

@staticmethod
def values() -> List[str]:
return [action.value for action in RolesAction]


class ClusterAction(str, _Action, Enum):
MANAGE_CLUSTER = "manage_cluster"
MANAGE_ROLES = "manage_roles"
READ_ROLES = "read_roles"

@staticmethod
def values() -> List[str]:
return [action.value for action in DatabaseAction]
return [action.value for action in ClusterAction]


class _Permission(BaseModel):
Expand All @@ -79,14 +76,15 @@ def _to_weaviate(self) -> WeaviatePermission:
return {"action": self.action, "collection": self.collection, "role": "*", "tenant": "*"}


class _DatabasePermission(_Permission):
action: DatabaseAction
class _RolesPermission(_Permission):
role: str
action: RolesAction

def _to_weaviate(self) -> WeaviatePermission:
return {"action": self.action, "collection": "*", "role": "*", "tenant": "*"}
return {"action": self.action, "collection": "*", "role": self.role, "tenant": "*"}


class _TenantPermission(_Permission):
class _TenantsPermission(_Permission):
collection: str
tenant: str
action: TenantsAction
Expand All @@ -113,12 +111,19 @@ class TenantsPermission:
action: TenantsAction


@dataclass
class RolesPermission:
role: str
action: RolesAction


@dataclass
class Role:
name: str
cluster_actions: Optional[List[ClusterAction]]
collections_permissions: Optional[List[CollectionsPermission]]
database_permissions: Optional[List[DatabaseAction]]
tenants_permissions: Optional[List[TenantsPermission]]
roles_permissions: Optional[List[RolesPermission]]


@dataclass
Expand All @@ -131,55 +136,69 @@ class User:


class ActionsFactory:
cluster = ClusterAction
collection = CollectionsAction
database = DatabaseAction
roles = RolesAction
tenants = TenantsAction


class PermissionsFactory:
@staticmethod
def collection(
collection: str, actions: Union[CollectionsAction, List[CollectionsAction]]
) -> Sequence[_CollectionsPermission]:
def collections(
*,
collection: Optional[str] = None,
actions: Union[CollectionsAction, List[CollectionsAction]]
) -> List[_CollectionsPermission]:
"""Create a permission specific to a collection to be used when creating and adding permissions to roles.
Granting this permission will implicitly grant all permissions on all tenants and objects in the collection.
For finer-grained control, use the `tenants` permission.
Args:
collection: The collection to grant permissions on.
actions: The actions to grant on the collection.
collection: The collection to grant permissions on. If not provided, the permission will be granted on all collections.
actions: The actions to grant on the collection permission.
Returns:
The collection permission.
"""
if isinstance(actions, CollectionsAction):
actions = [actions]

return [_CollectionsPermission(collection=collection, action=action) for action in actions]
return [
_CollectionsPermission(collection=collection or "*", action=action)
for action in actions
]

@staticmethod
def database(
actions: Union[DatabaseAction, List[DatabaseAction]]
) -> Sequence[_DatabasePermission]:
"""Create a database permission to be used when creating and adding permissions to roles.
def roles(
*, role: Optional[str] = None, actions: Union[RolesAction, List[RolesAction]]
) -> List[_RolesPermission]:
"""Create a roles permission to be used when creating and adding permissions to roles.
Args:
actions: The actions to grant on the database.
role: The role to grant permissions on. If not provided, the permission will be granted on all roles.
actions: The actions to grant on the roles permission.
Returns:
The database permission.
The role permission.
"""
if isinstance(actions, DatabaseAction):
if isinstance(actions, RolesAction):
actions = [actions]

return [_DatabasePermission(action=action) for action in actions]
return [_RolesPermission(action=action, role=role or "*") for action in actions]

@staticmethod
def tenant(
collection: str, tenant: str, actions: Union[TenantsAction, List[TenantsAction]]
) -> Sequence[_TenantPermission]:
def tenants(
*,
collection: str,
tenant: Optional[str] = None,
actions: Union[TenantsAction, List[TenantsAction]]
) -> List[_TenantsPermission]:
"""Create a tenant permission to be used when creating and adding permissions to roles.
Args:
collection: The collection to grant permissions on.
tenant: The tenant to grant permissions on.1
tenant: The tenant to grant permissions on. If not provided, the permission will be granted on all tenants in this collection.
actions: The actions to grant on the tenant.
Returns:
Expand All @@ -189,7 +208,7 @@ def tenant(
actions = [actions]

return [
_TenantPermission(collection=collection, tenant=tenant, action=action)
_TenantsPermission(collection=collection, tenant=tenant or "*", action=action)
for action in actions
]

Expand Down
22 changes: 16 additions & 6 deletions weaviate/rbac/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from weaviate.connect import ConnectionV4
from weaviate.connect.v4 import _ExpectedStatusCodes
from weaviate.rbac.models import (
ClusterAction,
CollectionsAction,
CollectionsPermission,
DatabaseAction,
RolesPermission,
Permissions,
_Permission,
RolesAction,
Role,
TenantsAction,
TenantsPermission,
Expand Down Expand Up @@ -111,19 +113,26 @@ def __init__(self, connection: ConnectionV4) -> None:
self.permissions = _Permissions(connection)

def __role_from_weaviate_role(self, role: WeaviateRole) -> Role:
cluster_actions: List[ClusterAction] = []
collection_permissions: List[CollectionsPermission] = []
database_permissions: List[DatabaseAction] = []
roles_permissions: List[RolesPermission] = []
tenant_permissions: List[TenantsPermission] = []
for permission in role["permissions"]:
if permission["action"] in CollectionsAction.values():
if permission["action"] in ClusterAction.values():
cluster_actions.append(ClusterAction(permission["action"]))
elif permission["action"] in CollectionsAction.values():
collection_permissions.append(
CollectionsPermission(
collection=permission["collection"],
action=CollectionsAction(permission["action"]),
)
)
elif permission["action"] in DatabaseAction.values():
database_permissions.append(DatabaseAction(permission["action"]))
elif permission["action"] in RolesAction.values():
roles_permissions.append(
RolesPermission(
role=permission["role"], action=RolesAction(permission["action"])
)
)
elif permission["action"] in TenantsAction.values():
tenant_permissions.append(
TenantsPermission(
Expand All @@ -138,8 +147,9 @@ def __role_from_weaviate_role(self, role: WeaviateRole) -> Role:
)
return Role(
name=role["name"],
cluster_actions=cluster_actions if len(cluster_actions) > 0 else None,
collections_permissions=cp if len(cp := collection_permissions) > 0 else None,
database_permissions=dp if len(dp := database_permissions) > 0 else None,
roles_permissions=rp if len(rp := roles_permissions) > 0 else None,
tenants_permissions=tp if len(tp := tenant_permissions) > 0 else None,
)

Expand Down

0 comments on commit 69b24bc

Please sign in to comment.