Skip to content

Commit

Permalink
Merge pull request #561 from TeskaLabs/feature/get-authorized-tenant
Browse files Browse the repository at this point in the history
Fix AuthService name and introduce get_authorized_tenant utility method
  • Loading branch information
byewokko committed Mar 28, 2024
2 parents ff5add7 + 2849174 commit e380d04
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 49 deletions.
73 changes: 41 additions & 32 deletions asab/web/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,18 @@
"email": "capybara1999@example.com",
# Authorized tenants and resources
"resources": {
# Globally granted resources
# Globally authorized resources
"*": [
"authz:superuser",
],
# Resources granted within the tenant "default"
# Resources authorized within the tenant "default"
"default": [
"authz:superuser",
"some-test-data:access",
],
# Resources granted within the tenant "test-tenant"
"test-tenant": [
"authz:superuser",
"cake:eat",
],
},
# Subject's assigned (not authorized!) tenants
# List of tenants that the user is a member of.
# These tenants are NOT AUTHORIZED!
"tenants": ["default", "test-tenant", "another-tenant"]
}

Expand All @@ -86,25 +82,26 @@ class AuthService(asab.Service):
Provides authentication and authorization of incoming requests.
Configuration:
- Configuration section: auth
- Configuration options:
- public_keys_url:
- default: ""
- URL location containing the authorization server's public JWK keys (often found at "/.well-known/jwks.json")
- enabled:
- default: "yes"
- The "enabled" option switches authentication and authorization on, off or activates mock mode. The default value is True (on).
- In MOCK MODE
- no authorization server is needed,
- all incoming requests are mock-authorized with pre-defined user info,
- custom mock user info can supplied in a JSON file.
-mock_user_info_path:
- default: "/conf/mock-userinfo.json"
Configuration section: auth
Configuration options:
public_keys_url:
- default: ""
- URL containing the authorization server's public JWKey set (usually found at "/.well-known/jwks.json")
enabled:
- default: "yes"
- options: "yes", "no", "mocked"
- Switch authentication and authorization on, off or activate mock mode.
- In MOCK MODE
- no authorization server is needed,
- all incoming requests are mock-authorized with pre-defined user info,
- custom mock user info can supplied in a JSON file.
mock_user_info_path:
- default: "/conf/mock-userinfo.json"
"""

_PUBLIC_KEYS_URL_DEFAULT = "http://localhost:3081/.well-known/jwks.json"

def __init__(self, app, service_name="asab.AuthzService"):
def __init__(self, app, service_name="asab.AuthService"):
super().__init__(app, service_name)
self.PublicKeysUrl = asab.Config.get("auth", "public_keys_url")

Expand Down Expand Up @@ -225,6 +222,17 @@ async def get_userinfo_from_id_token(self, bearer_token):
raise asab.exceptions.NotAuthenticatedError()


def get_authorized_tenant(self, request) -> typing.Optional[str]:
"""
Get the request's authorized tenant.
"""
if hasattr(request, "_AuthorizedTenants"):
for tenant in request._AuthorizedTenants:
# Return the first authorized tenant
return tenant
return None


def has_superuser_access(self, authorized_resources: typing.Iterable) -> bool:
"""
Check if the superuser resource is present in the authorized resource list.
Expand Down Expand Up @@ -343,20 +351,20 @@ async def wrapper(*args, **kwargs):
assert user_info is not None
request._UserInfo = user_info
resource_dict = request._UserInfo["resources"]
request._Resources = frozenset(resource_dict.get("*", []))
request._Tenants = frozenset(t for t in resource_dict.keys() if t != "*")
request._AuthorizedResources = frozenset(resource_dict.get("*", []))
request._AuthorizedTenants = frozenset(t for t in resource_dict.keys() if t != "*")
else:
request._UserInfo = None
request._Resources = None
request._Tenants = None
request._AuthorizedResources = None
request._AuthorizedTenants = None

# Add access control methods to the request
def has_resource_access(*required_resources: list) -> bool:
return self.has_resource_access(request._Resources, required_resources)
return self.has_resource_access(request._AuthorizedResources, required_resources)
request.has_resource_access = has_resource_access

def has_superuser_access() -> bool:
return self.has_superuser_access(request._Resources)
return self.has_superuser_access(request._AuthorizedResources)
request.has_superuser_access = has_superuser_access

return await handler(*args, **kwargs)
Expand Down Expand Up @@ -428,12 +436,13 @@ def _authorize_tenant_request(self, request, tenant):
Check access to requested tenant and add tenant resources to the request
"""
# Check if tenant access is authorized
if tenant not in request._Tenants:
if tenant not in request._AuthorizedTenants:
L.warning("Tenant not authorized.", struct_data={"tenant": tenant, "sub": request._UserInfo.get("sub")})
raise asab.exceptions.AccessDeniedError()

# Extend globally granted resources with tenant-granted resources
request._Resources = frozenset(request._Resources.union(request._UserInfo["resources"].get(tenant, [])))
request._AuthorizedResources = set(request._AuthorizedResources.union(
request._UserInfo["resources"].get(tenant, [])))


def _add_tenant_from_path(self, handler):
Expand Down Expand Up @@ -559,5 +568,5 @@ def _add_resources(handler):
@functools.wraps(handler)
async def wrapper(*args, **kwargs):
request = args[-1]
return await handler(*args, resources=request._Resources, **kwargs)
return await handler(*args, resources=request._AuthorizedResources, **kwargs)
return wrapper
40 changes: 23 additions & 17 deletions examples/web-auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,29 @@
import asab.web.auth
import typing

# Set up a web container listening at port 8080
asab.Config["web"] = {"listen": "8080"}

# Disables or enables all authentication and authorization, or switches it into MOCK mode.
# When disabled, the `resources` and `userinfo` handler arguments are set to `None`.
asab.Config["auth"]["enabled"] = "mock" # Mock authorization, useful for debugging.
# asab.Config["auth"]["enabled"] = "yes" # Authorization is enabled.
# asab.Config["auth"]["enabled"] = "no" # Authorization is disabled.

# Activating the mock mode disables communication with the authorization server.
# The requests' Authorization headers are ignored and AuthService provides mock authorization with mock user info.
# You can provide custom user info by specifying the path pointing to your JSON file.
asab.Config["auth"]["mock_user_info_path"] = "./mock-userinfo.json"

# URL of the authorization server's JWK public keys, used for ID token verification.
# This option is ignored in mock mode or when authorization is disabled.
asab.Config["auth"]["public_keys_url"] = "http://localhost:3081/.well-known/jwks.json"
if "web" not in asab.Config:
asab.Config["web"] = {
# Set up a web container listening at port 8080
"listen": "8080"
}

if "auth" not in asab.Config:
asab.Config["auth"] = {
# Disable or enable all authentication and authorization, or switch into MOCK mode.
# When disabled, the `resources` and `userinfo` handler arguments are set to `None`.
"enabled": "mock", # Mock authorization, useful for debugging.
# "enabled": "yes", # Authorization is enabled.
# "enabled": "no", # Authorization is disabled.

# Activating the mock mode disables communication with the authorization server.
# The requests' Authorization headers are ignored and AuthService provides mock authorization with mock user info.
# You can provide custom user info by specifying the path pointing to your JSON file.
"mock_user_info_path": "./mock-userinfo.json",

# URL of the authorization server's JWK public keys, used for ID token verification.
# This option is ignored in mock mode or when authorization is disabled.
"public_keys_url": "http://localhost:3081/.well-known/jwks.json",
}


class MyApplication(asab.Application):
Expand Down

0 comments on commit e380d04

Please sign in to comment.