Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Authz contextvar and Authorization object #615

Merged
merged 49 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e352185
add Authz contextvar
byewokko Oct 2, 2024
69e1cb2
add Authorization object
byewokko Oct 2, 2024
702afec
move rbac functions to authz
byewokko Oct 2, 2024
08e151f
refactor auth service, use Authz context var
byewokko Oct 2, 2024
a932ab8
lint and debug
byewokko Oct 3, 2024
3c23b72
Merge branch 'master' into feature/auth-contextvar
byewokko Oct 3, 2024
b27fb76
make Authorization printable, debug
byewokko Oct 3, 2024
b047b6d
add typing
byewokko Oct 3, 2024
547528b
rename is_superuser
byewokko Oct 3, 2024
995e717
flake8
byewokko Oct 3, 2024
e298f64
add comments with types
byewokko Oct 7, 2024
4c23e3b
add docstrings
byewokko Oct 7, 2024
283ae16
delete deprecated asab.web.authz module
byewokko Oct 7, 2024
066d2bc
handle userinfo none
byewokko Oct 7, 2024
713182e
more cleanup, debug authservice disabled, deprecate resources and use…
byewokko Oct 7, 2024
a58730e
explaining comments
byewokko Oct 7, 2024
32faa10
return None to prevent smooth flow
byewokko Oct 7, 2024
5c89a1d
remove redundant return value
byewokko Oct 7, 2024
fd1fda2
require-methods
byewokko Oct 7, 2024
9ef2a30
add todo
byewokko Oct 7, 2024
f9f4046
use tenant from context; add expiration and validity; update docstrings
byewokko Oct 8, 2024
868cba9
always use tenant from the context
byewokko Oct 8, 2024
9bf8b3a
Authorization factory
byewokko Oct 8, 2024
91d7d08
fix validity; extend repr; log authz errors
byewokko Oct 8, 2024
1dc62c3
use ready method
byewokko Oct 8, 2024
1877244
no Authorization when AuthService is disabled
byewokko Oct 8, 2024
5544325
authorization refactoring
byewokko Oct 8, 2024
e84052c
update auth example
byewokko Oct 8, 2024
b583e89
Merge branch 'master' into feature/auth-contextvar
byewokko Oct 8, 2024
58c6031
Authorization validate method
byewokko Oct 9, 2024
332e2b9
Merge branch 'master' into feature/auth-contextvar
byewokko Oct 10, 2024
f0b10c9
re-introduce is_valid (necessary for cleanup)
byewokko Oct 10, 2024
de2322d
auto-install authorization middleware
byewokko Oct 10, 2024
95ff62f
update auth docs
byewokko Oct 10, 2024
a3100d2
renaming, docstrings
byewokko Oct 11, 2024
7cf6a07
documentation
byewokko Oct 11, 2024
5267ffd
cleanup
byewokko Oct 11, 2024
b878a70
cleanup, naming
byewokko Oct 11, 2024
771959d
separate tenant extraction for websocket
byewokko Oct 14, 2024
c994810
Merge branch 'master' into feature/auth-contextvar
byewokko Oct 14, 2024
365365b
fix import
byewokko Oct 14, 2024
7ca59c0
Merge branch 'master' into feature/auth-contextvar
byewokko Oct 17, 2024
0c160bc
split auto-install method
byewokko Oct 22, 2024
d183de8
warn at duplicate installation
byewokko Oct 22, 2024
0ebf25b
warn when no container has authorization installed after initialization
byewokko Oct 22, 2024
83dbf01
more descriptive logging on authservice init
byewokko Oct 23, 2024
791fb80
more descriptive logging on authservice init
byewokko Oct 23, 2024
1c278a7
remove obsolete installation
byewokko Oct 23, 2024
abeb5b3
make UserInfo attr protected
byewokko Oct 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions asab/contextvars.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import contextvars

# Contains tenant ID string
Tenant = contextvars.ContextVar("Tenant")

# Contains asab.web.auth.Authorization object
Authz = contextvars.ContextVar("Authz")
2 changes: 2 additions & 0 deletions asab/web/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from .decorator import require, noauth
from .service import AuthService
from .authz import Authorization

__all__ = (
"AuthService",
"Authorization",
"require",
"noauth",
)
145 changes: 145 additions & 0 deletions asab/web/auth/authz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import typing
import logging

from asab.exceptions import AccessDeniedError

L = logging.getLogger(__name__)


SUPERUSER_RESOURCE_ID = "authz:superuser"


class Authorization:
"""
Contains authentication and authorization details, provides methods for checking access control
"""
def __init__(self, auth_service, userinfo: dict, tenant: typing.Union[str, None]):
self.AuthService = auth_service
if self.AuthService.is_enabled() and userinfo is None:
L.error("Userinfo is mandatory when AuthService is enabled.")
raise AccessDeniedError()
self.UserInfo = userinfo or {}
self.Tenant = tenant

self.CredentialsId = self.UserInfo.get("sub")
self.Username = self.UserInfo.get("preferred_username") or self.UserInfo.get("username")
self.Email = self.UserInfo.get("email")
self.Phone = self.UserInfo.get("phone")

self.Issuer = self.UserInfo.get("iss") # Who issued the authorization
self.AuthorizedParty = self.UserInfo.get("azp") # What party (application) is authorized


def __repr__(self):
return "<Authorization [{}cid: {!r}, azp: {!r}]>".format(
"SUPERUSER, " if self.is_superuser() else "",
self.CredentialsId,
self.AuthorizedParty,
)


def require_superuser(self):
if not self.is_superuser():
raise AccessDeniedError()


def require_resource_access(self, resource_id: typing.Union[str, typing.Iterable[str]]):
if not self.has_resource_access(resource_id):
raise AccessDeniedError()


def is_superuser(self) -> bool:
"""
Check whether the agent is a superuser.

:return: Is the agent a superuser?
"""
if not self.AuthService.is_enabled():
# Authorization is disabled = everything is allowed
return True

return is_superuser(self.UserInfo)


def has_resource_access(self, resource_id: typing.Union[str, typing.Iterable[str]]) -> bool:
"""
Check whether the agent is authorized to access a resource or multiple resources.

:param resource_id: Resource ID or a list of resource IDs whose authorization is required.
:return: Is resource access authorized?
"""
if not self.AuthService.is_enabled():
# Authorization is disabled = everything is allowed
return True

if isinstance(resource_id, str):
return has_resource_access(self.UserInfo, {resource_id}, tenant=self.Tenant)
else:
return has_resource_access(self.UserInfo, resource_id, tenant=self.Tenant)


def authorized_resources(self) -> typing.Optional[typing.Set[str]]:
"""
Return the set of EXPLICITLY authorized resources. (Use carefully with superusers.)

NOTE: If possible, use methods has_resource_access(resource_id) and is_superuser() instead of inspecting
the set of resources.

:return: Set of authorized resources.
"""
if not self.AuthService.is_enabled():
# Authorization is disabled = authorized resources are unknown
return None

return get_authorized_resources(self.UserInfo, self.Tenant)


def is_superuser(user_info: typing.Mapping) -> bool:
"""
Check if the superuser resource is present in the authorized resource list.
"""
return SUPERUSER_RESOURCE_ID in get_authorized_resources(user_info, tenant=None)


def has_resource_access(
user_info: typing.Mapping,
resource_ids: typing.Iterable,
tenant: typing.Union[str, None],
) -> bool:
"""
Check if the requested resources or the superuser resource are present in the authorized resource list.
"""
if is_superuser(user_info):
return True

authorized_resources = get_authorized_resources(user_info, tenant)
for resource in resource_ids:
if resource not in authorized_resources:
return False

return True


def has_tenant_access(user_info: typing.Mapping, tenant: str) -> bool:
"""
Check the agent's userinfo to see if they are authorized to access a tenant.
If the agent has superuser access, tenant access is always implicitly granted.
"""
if tenant == "*":
raise ValueError("Invalid tenant name: '*'")
if is_superuser(user_info):
return True
if tenant in user_info.get("resources", {}):
return True
return False


def get_authorized_resources(user_info: typing.Mapping, tenant: typing.Union[str, None]) -> typing.Set[str]:
"""
Extract resources authorized within given tenant (or globally, if tenant is None).

:param user_info:
:param tenant:
:return: Set of authorized resources.
"""
return set(user_info.get("resources", {}).get(tenant if tenant is not None else "*", []))
12 changes: 5 additions & 7 deletions asab/web/auth/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import inspect

from ...exceptions import AccessDeniedError
from ...contextvars import Authz

#

Expand Down Expand Up @@ -32,14 +33,11 @@ def decorator_require(handler):

@functools.wraps(handler)
async def wrapper(*args, **kwargs):
request = args[-1]

if not hasattr(request, "has_resource_access"):
raise Exception(
"Cannot check resource access. Make sure that AuthService is installed and that "
"the handler method does not use both the '@noauth' and the '@require' decorators at once.")
authz = Authz.get()
if authz is None:
raise AccessDeniedError()

if not request.has_resource_access(*resources):
if not authz.has_resource_access(resources):
raise AccessDeniedError()

return await handler(*args, **kwargs)
Expand Down
Loading
Loading