Skip to content

Commit

Permalink
Merge pull request #466 from TeskaLabs/feature/config-disable-auth
Browse files Browse the repository at this point in the history
Add config switch to disable auth
  • Loading branch information
byewokko authored Aug 10, 2023
2 parents 1b6d080 + c1f9f1b commit a488344
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 61 deletions.
108 changes: 64 additions & 44 deletions asab/web/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
# Whether the app is tenant-aware
"multitenancy": "yes",

# Whether the authentication and authorization are enabled
"enabled": "yes",

# In DEV MODE
# - no authorization server is needed,
# - all incoming requests are "authorized" with custom mocked user info data loaded from a JSON file,
Expand All @@ -96,51 +99,54 @@ class AuthService(asab.Service):
def __init__(self, app, service_name="asab.AuthzService"):
super().__init__(app, service_name)
self.MultitenancyEnabled = asab.Config.getboolean("auth", "multitenancy")
self.PublicKeysUrl = asab.Config.get("auth", "public_keys_url")

self.AuthEnabled = asab.Config.getboolean("auth", "enabled")
self.DevModeEnabled = asab.Config.getboolean("auth", "dev_mode")
if self.DevModeEnabled:
# Load custom user info
dev_user_info_path = asab.Config.get("auth", "dev_user_info_path")
if os.path.isfile(dev_user_info_path):
with open(dev_user_info_path, "rb") as fp:
self.DevUserInfo = json.load(fp)
else:
self.DevUserInfo = DEV_USERINFO_DEFAULT

# Validate user info
resources = self.DevUserInfo.get("resources", {})
if not isinstance(resources, dict) or not all(
map(lambda kv: isinstance(kv[0], str) and isinstance(kv[1], list), resources.items())
):
raise ValueError("User info 'resources' must be an object with string keys and array values.")
self.PublicKeysUrl = asab.Config.get("auth", "public_keys_url")

if not self.AuthEnabled:
pass
elif self.DevModeEnabled:
self.DevUserInfo = self._prepare_dev_user_info()
elif jwcrypto is None:
raise ModuleNotFoundError(
"You are trying to use asab.web.auth module without 'jwcrypto' installed. "
"Please run 'pip install jwcrypto' "
"or install asab with 'authz' optional dependency.")
elif len(self.PublicKeysUrl) == 0:
self.PublicKeysUrl = self._PUBLIC_KEYS_URL_DEFAULT
L.warning(
"AuthService is running in DEV MODE. All web requests will be authorized with mock user info, which "
"currently grants access to the following tenants: {}. To customize dev mode authorization (add or "
"remove tenants and resources, change username etc.), provide your own user info in {!r}.".format(
list(t for t in self.DevUserInfo.get("resources", {}).keys() if t != "*"),
dev_user_info_path))
else:
if len(self.PublicKeysUrl) == 0:
self.PublicKeysUrl = self._PUBLIC_KEYS_URL_DEFAULT
L.warning(
"No 'public_keys_url' provided in [auth] config section. "
"Defaulting to {!r}.".format(self._PUBLIC_KEYS_URL_DEFAULT))
if jwcrypto is None:
raise ModuleNotFoundError(
"You are trying to use asab.web.authz without 'jwcrypto' installed. "
"Please run 'pip install jwcrypto' "
"or install asab with 'authz' optional dependency.")
"No 'public_keys_url' provided in [auth] config section. "
"Defaulting to {!r}.".format(self._PUBLIC_KEYS_URL_DEFAULT))

self.AuthServerPublicKey = None # TODO: Support multiple public keys
# Limit the frequency of auth server requests to save network traffic
self.AuthServerCheckCooldown = datetime.timedelta(minutes=5)
self.AuthServerLastSuccessfulCheck = None

def _prepare_dev_user_info(self):
# Load custom user info
dev_user_info_path = asab.Config.get("auth", "dev_user_info_path")
if os.path.isfile(dev_user_info_path):
with open(dev_user_info_path, "rb") as fp:
user_info = json.load(fp)
else:
user_info = DEV_USERINFO_DEFAULT
# Validate user info
resources = self.DevUserInfo.get("resources", {})
if not isinstance(resources, dict) or not all(
map(lambda kv: isinstance(kv[0], str) and isinstance(kv[1], list), resources.items())
):
raise ValueError("User info 'resources' must be an object with string keys and array values.")
L.warning(
"AuthService is running in DEV MODE. All web requests will be authorized with mock user info, which "
"currently grants access to the following tenants: {}. To customize dev mode authorization (add or "
"remove tenants and resources, change username etc.), provide your own user info in {!r}.".format(
list(t for t in self.DevUserInfo.get("resources", {}).keys() if t != "*"),
dev_user_info_path))
return user_info

async def initialize(self, app):
if self.DevModeEnabled is False:
if self.AuthEnabled and not self.DevModeEnabled:
await self._fetch_public_keys_if_needed()


Expand All @@ -159,7 +165,9 @@ def is_ready(self):
"""
Check if the service is ready to authorize requests.
"""
if self.DevModeEnabled is True:
if not self.AuthEnabled:
return True
if self.DevModeEnabled:
return True
if self.AuthServerPublicKey is None:
return False
Expand Down Expand Up @@ -196,13 +204,17 @@ def has_superuser_access(self, authorized_resources: typing.Iterable) -> bool:
"""
Check if the superuser resource is present in the authorized resource list.
"""
if not self.AuthEnabled:
return True
return SUPERUSER_RESOURCE in authorized_resources


def has_resource_access(self, authorized_resources: typing.Iterable, required_resources: typing.Iterable) -> bool:
"""
Check if the requested resources or the superuser resource are present in the authorized resource list.
"""
if not self.AuthEnabled:
return True
if self.has_superuser_access(authorized_resources):
return True
for resource in required_resources:
Expand Down Expand Up @@ -274,20 +286,26 @@ def _authenticate_request(self, handler):
@functools.wraps(handler)
async def wrapper(*args, **kwargs):
request = args[-1]
if self.DevModeEnabled:
if not self.AuthEnabled:
user_info = None
elif self.DevModeEnabled:
user_info = self.DevUserInfo
else:
# Extract user info from the request Authorization header
bearer_token = _get_bearer_token(request)
user_info = await self.get_userinfo_from_id_token(bearer_token)

assert user_info is not None

# Add userinfo, tenants and global resources to the request
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 != "*")
if self.AuthEnabled:
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 != "*")
else:
request._UserInfo = None
request._Resources = None
request._Tenants = None

# Add access control methods to the request
def has_resource_access(*required_resources: list) -> bool:
Expand Down Expand Up @@ -386,7 +404,8 @@ def _add_tenant_from_path(self, handler):
async def wrapper(*args, **kwargs):
request = args[-1]
tenant = request.match_info["tenant"]
self._authorize_tenant_request(request, tenant)
if self.AuthEnabled:
self._authorize_tenant_request(request, tenant)
return await handler(*args, tenant=tenant, **kwargs)

return wrapper
Expand All @@ -407,7 +426,8 @@ async def wrapper(*args, **kwargs):
tenant = None
else:
tenant = request.query["tenant"]
self._authorize_tenant_request(request, tenant)
if self.AuthEnabled:
self._authorize_tenant_request(request, tenant)
return await handler(*args, tenant=tenant, **kwargs)

return wrapper
Expand Down
63 changes: 46 additions & 17 deletions examples/web-auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
import typing

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

# Disables or enables all authentication and authorization.
# When disabled, the `resources` and `userinfo` handler arguments are set to `None`.
asab.Config["auth"]["enabled"] = "yes"

# Changes the behavior of endpoints with configurable tenant parameter.
# With multitenancy enabled, the tenant paramerter in query is required.
# With multitenancy disabled, the tenant paramerter in query is ignored.
# With multitenancy enabled, the `tenant` paramerter in query is required.
# With multitenancy disabled, the `tenant` paramerter in query is ignored.
asab.Config["auth"]["multitenancy"] = "yes"

# Activating the dev mode disables communication with the authorization server.
Expand Down Expand Up @@ -73,7 +77,7 @@ async def no_auth(self, request):
return asab.web.rest.json_response(request, data)


async def auth(self, request, *, user_info: dict, resources: frozenset):
async def auth(self, request, *, user_info: typing.Optional[dict], resources: typing.Optional[frozenset]):
"""
TENANT-AGNOSTIC
- returns 401 if authentication not successful
Expand All @@ -84,14 +88,14 @@ async def auth(self, request, *, user_info: dict, resources: frozenset):
"""
data = {
"tenant": "NOT AVAILABLE",
"resources": list(resources),
"resources": list(resources) if resources else None,
"user_info": user_info,
}
return asab.web.rest.json_response(request, data)


@asab.web.auth.require("something:access", "something:edit")
async def auth_resource(self, request, *, user_info: dict, resources: frozenset):
async def auth_resource(self, request, *, user_info: typing.Optional[dict], resources: typing.Optional[frozenset]):
"""
TENANT-AGNOSTIC + RESOURCE CHECK
- returns 401 if authentication not successful
Expand All @@ -104,7 +108,7 @@ async def auth_resource(self, request, *, user_info: dict, resources: frozenset)
"""
data = {
"tenant": "NOT AVAILABLE",
"resources": list(resources),
"resources": list(resources) if resources else None,
"user_info": user_info,
}
return asab.web.rest.json_response(request, data)
Expand All @@ -114,20 +118,30 @@ async def auth_resource(self, request, *, user_info: dict, resources: frozenset)
"type": "object"
})
@asab.web.auth.require("something:access", "something:edit")
async def auth_resource_put(self, request, *, user_info: dict, resources: frozenset, json_data: dict):
async def auth_resource_put(
self, request, *,
user_info: typing.Optional[dict],
resources: typing.Optional[frozenset],
json_data: dict
):
"""
Decorator asab.web.auth.require can be used together with other decorators.
"""
data = {
"tenant": "NOT AVAILABLE",
"resources": list(resources),
"resources": list(resources) if resources else None,
"user_info": user_info,
"json_data": json_data,
}
return asab.web.rest.json_response(request, data)


async def tenant_in_path(self, request, *, tenant: str, user_info: dict, resources: frozenset):
async def tenant_in_path(
self, request, *,
tenant: typing.Optional[str],
user_info: typing.Optional[dict],
resources: typing.Optional[frozenset]
):
"""
TENANT-AWARE
- returns 401 if authentication not successful
Expand All @@ -140,13 +154,18 @@ async def tenant_in_path(self, request, *, tenant: str, user_info: dict, resourc
"""
data = {
"tenant": tenant,
"resources": list(resources),
"resources": list(resources) if resources else None,
"user_info": user_info,
}
return asab.web.rest.json_response(request, data)


async def tenant_in_query(self, request, *, tenant: typing.Union[str|None], user_info: dict, resources: frozenset):
async def tenant_in_query(
self, request, *,
tenant: typing.Optional[str],
user_info: typing.Optional[dict],
resources: typing.Optional[frozenset]
):
"""
CONFIGURABLY TENANT-AWARE
- returns 401 if authentication not successful
Expand All @@ -165,14 +184,19 @@ async def tenant_in_query(self, request, *, tenant: typing.Union[str|None], user
"""
data = {
"tenant": tenant,
"resources": list(resources),
"resources": list(resources) if resources else None,
"user_info": user_info,
}
return asab.web.rest.json_response(request, data)


@asab.web.auth.require("something:access", "something:edit")
async def tenant_in_path_resources(self, request, *, tenant: typing.Union[str|None], user_info: dict, resources: frozenset):
async def tenant_in_path_resources(
self, request, *,
tenant: typing.Optional[str],
user_info: typing.Optional[dict],
resources: typing.Optional[frozenset]
):
"""
TENANT-AWARE + RESOURCE CHECK
- returns 401 if authentication not successful
Expand All @@ -187,14 +211,19 @@ async def tenant_in_path_resources(self, request, *, tenant: typing.Union[str|No
"""
data = {
"tenant": tenant,
"resources": list(resources),
"resources": list(resources) if resources else None,
"user_info": user_info,
}
return asab.web.rest.json_response(request, data)


@asab.web.auth.require("something:access", "something:edit")
async def tenant_in_query_resources(self, request, *, tenant: typing.Union[str|None], user_info: dict, resources: frozenset):
async def tenant_in_query_resources(
self, request, *,
tenant: typing.Optional[str],
user_info: typing.Optional[dict],
resources: typing.Optional[frozenset]
):
"""
CONFIGURABLY TENANT-AWARE + RESOURCE CHECK
- returns 401 if authentication not successful
Expand All @@ -215,7 +244,7 @@ async def tenant_in_query_resources(self, request, *, tenant: typing.Union[str|N
"""
data = {
"tenant": tenant,
"resources": list(resources),
"resources": list(resources) if resources else None,
"user_info": user_info,
}
return asab.web.rest.json_response(request, data)
Expand Down

0 comments on commit a488344

Please sign in to comment.