Skip to content

Commit

Permalink
Merge pull request #395 from TeskaLabs/feature/shared-roles
Browse files Browse the repository at this point in the history
Global roles propagated to tenants
  • Loading branch information
byewokko authored Jul 30, 2024
2 parents 08de363 + 14a3a09 commit f069222
Show file tree
Hide file tree
Showing 15 changed files with 730 additions and 170 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
## v24.29

### Pre-releases
- v24.29-alpha2
- v24.29-alpha3
- v24.29-alpha2
- v24.29-alpha1

### Fix
- Handle session decryption error (#410, `v24.29-alpha2`)

### Features
- Global roles propagated to tenants (#395, `v24.29-alpha3`)
- Sort clients alphabetically by client name (#408, `v24.29-alpha2`)
- Print ASAB version during Docker build (#406, `v24.29-alpha1`)

Expand Down
30 changes: 30 additions & 0 deletions docs/reference/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,33 @@ title: Roles
---

# Roles

## Tenant roles

Tenant roles exist and have effect only within a specific tenant.
They can only be assigned to users that are members of that tenant.
Their ID always starts with the name of their tenant followed by slash and the role name (`$TENANT_ID/$ROLE_NAME`),
for example `acmecorp/admin`.

To edit the privileges of a tenant role, it is necessary to have access to resource `seacat:role:edit`.

## Global roles

Global roles exist above tenants.
When assigned to a user, a global role has effect across all the user's tenants.
Their ID starts with an asterisk followed by slash and the role name (`*/$ROLE_NAME`),
for example `*/superuser`.

To edit the privileges of a global role, it is necessary to have superuser privileges.

### Global roles with tenant propagation

When creating a global role, there is an option to mark it as "Propagated to tenants".
Propagated global roles behave as common global roles, plus they create their virtual copy (or a "link", to be more precise) in every tenant.
This allows for defining a role globally, while also being able to assign it to users independently in every tenant.
The ID of such virtual propagated role starts with the name of their tenant followed by slash, **a tilde** and the global role name (`$TENANT_ID/~$ROLE_NAME`),
for example if the global role is called `*/reader`, its virtual copy in tenant `acmecorp` will have the ID `acmecorp/~reader`.

The privileges of virtual tenant roles are not editable, they are always in sync with their global role.
To change the privileges, it is necessary to edit the global role.
The changes will be propagated to all tenants.
4 changes: 2 additions & 2 deletions seacatauth/authz/resource/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ async def delete(self, resource_id: str, hard_delete: bool = False):

# Remove the resource from all roles
role_svc = self.App.get_service("seacatauth.RoleService")
roles = await role_svc.list(resource=resource_id)
roles = await role_svc.list(resource_filter=resource_id)
if roles["count"] > 0:
for role in roles["data"]:
await role_svc.update(role["_id"], resources_to_remove=[resource_id])
Expand Down Expand Up @@ -296,7 +296,7 @@ async def rename(self, resource_id: str, new_resource_id: str):
raise asab.exceptions.ValidationError("Built-in resource cannot be renamed")

role_svc = self.App.get_service("seacatauth.RoleService")
roles = await role_svc.list(resource=resource_id)
roles = await role_svc.list(resource_filter=resource_id)

# Delete existing resource
await self.StorageService.delete(self.ResourceCollection, resource_id)
Expand Down
83 changes: 57 additions & 26 deletions seacatauth/authz/role/handler/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import asab
import asab.web.rest
import asab.storage.exceptions
import asab.exceptions

from .... import exceptions
from ....decorators import access_control
from .... import generic

#

Expand Down Expand Up @@ -90,24 +92,17 @@ async def list(self, request):
enum:
- true
"""
tenant_id = request.match_info["tenant"]
if tenant_id == "*":
asab.web.rest.json_response(request, {"result": "ACCESS-DENIED"}, status=403)
return await self._list(request, tenant_id=tenant_id)
return await self._list(request, tenant_id=request.match_info["tenant"])

async def _list(self, request, *, tenant_id):
page = int(request.query.get("p", 1)) - 1
limit = request.query.get("i")
if limit is not None:
limit = int(limit)
filter_string = request.query.get("f")
resource = request.query.get("resource")
exclude_global = request.query.get("exclude_global", "false") == "true"

search = generic.SearchParams(request.query)
result = await self.RoleService.list(
tenant_id, page, limit, filter_string,
resource=resource,
exclude_global=exclude_global)
tenant_id=tenant_id,
page=search.Page,
limit=search.ItemsPerPage,
name_filter=search.SimpleFilter,
resource_filter=search.get("resource"),
)
return asab.web.rest.json_response(request, result)


Expand All @@ -126,15 +121,44 @@ async def get(self, request):
return asab.web.rest.json_response(request, result)


@asab.web.rest.json_schema_handler({
"type": "object",
"additionalProperties": False,
"properties": {
"label": {"type": "string"},
"description": {"type": "string"},
"propagated": {"type": "boolean"},
"resources": {
"type": "array",
"items": {"type": "string"},
},
}
})
@access_control("seacat:role:edit")
async def create(self, request, *, tenant):
async def create(self, request, *, tenant, json_data):
"""
Create a new role
"""
role_name = request.match_info["role_name"]
role_id = "{}/{}".format(tenant, role_name)
role_id = await self.RoleService.create(role_id)
return asab.web.rest.json_response(request, {"result": "OK", "id": role_id})
try:
role_id = await self.RoleService.create(role_id, **json_data)
except exceptions.ResourceNotFoundError as e:
return asab.web.rest.json_response(request, status=404, data={
"result": "ERROR",
"tech_err": "Resource not found.",
"err_dict": {"resource_id": e.ResourceId},
})
except asab.exceptions.Conflict:
return asab.web.rest.json_response(request, status=409, data={
"result": "ERROR",
"tech_err": "Role already exists.",
"err_dict": {"role_id": role_id},
})
return asab.web.rest.json_response(request, {
"result": "OK",
"id": role_id
})


@access_control("seacat:role:edit")
Expand All @@ -147,18 +171,21 @@ async def delete(self, request, *, tenant):

try:
result = await self.RoleService.delete(role_id)
except KeyError:
L.log(asab.LOG_NOTICE, "Role not found", struct_data={"role_id": role_id})
return aiohttp.web.HTTPNotFound()
except exceptions.RoleNotFoundError:
return asab.web.rest.json_response(request, status=404, data={
"result": "ERROR", "tech_err": "Role not found."})
except exceptions.NotEditableError:
return asab.web.rest.json_response(request, status=405, data={
"result": "ERROR", "tech_err": "Role is not editable."})
return asab.web.rest.json_response(request, result)


@asab.web.rest.json_schema_handler({
"type": "object",
"additionalProperties": False,
"properties": {
"description": {
"type": "string"},
"label": {"type": "string"},
"description": {"type": "string"},
"add": {
"type": "array",
"items": {"type": "string"},
Expand Down Expand Up @@ -197,12 +224,16 @@ async def update(self, request, *, json_data, tenant):
try:
result = await self.RoleService.update(
role_id,
label=json_data.get("label"),
description=json_data.get("description"),
resources_to_set=resources_to_set,
resources_to_add=resources_to_add,
resources_to_remove=resources_to_remove,
)
except exceptions.RoleNotFoundError as e:
L.log(asab.LOG_NOTICE, "Role not found", struct_data={"role_id": e.Role})
return aiohttp.web.HTTPNotFound()
except exceptions.RoleNotFoundError:
return asab.web.rest.json_response(request, status=404, data={
"result": "ERROR", "tech_err": "Role not found."})
except exceptions.NotEditableError:
return asab.web.rest.json_response(request, status=405, data={
"result": "ERROR", "tech_err": "Role is not editable."})
return asab.web.rest.json_response(request, data={"result": result})
Loading

0 comments on commit f069222

Please sign in to comment.