Add resource provided registry and plugin system
This commit is contained in:
parent
e56be3c378
commit
64f6a90f22
10 changed files with 229 additions and 142 deletions
|
@ -5,23 +5,23 @@ import logging
|
|||
from authlib.oauth2.auth import OAuth2Token
|
||||
from httpx import AsyncClient
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||
from fastapi import FastAPI, HTTPException, Depends, status
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
# from starlette.middleware.sessions import SessionMiddleware
|
||||
# from authlib.integrations.starlette_client.apps import StarletteOAuth2App
|
||||
# from authlib.oauth2.rfc6749 import OAuth2Token
|
||||
|
||||
from .auth.provider import Provider
|
||||
from .auth.utils import (
|
||||
from oidc_test.auth.provider import Provider
|
||||
from oidc_test.auth.utils import (
|
||||
get_token_or_none,
|
||||
get_user_from_token,
|
||||
UserWithRole,
|
||||
)
|
||||
|
||||
from .auth_providers import providers
|
||||
from .settings import settings
|
||||
from .models import User
|
||||
from oidc_test.auth_providers import providers
|
||||
from oidc_test.settings import settings
|
||||
from oidc_test.models import User
|
||||
from oidc_test.registry import ProcessError, ProcessResult, registry
|
||||
|
||||
logger = logging.getLogger("oidc-test")
|
||||
|
||||
|
@ -91,6 +91,128 @@ async def get_protected_by_foorole_or_barrole(
|
|||
return {"msg": "Only users with foorole or barrole can see this"}
|
||||
|
||||
|
||||
@resource_server.get("/{resource_name}")
|
||||
@resource_server.get("/{resource_name}/{resource_id}")
|
||||
async def get_resource(
|
||||
resource_name: str,
|
||||
user: Annotated[User, Depends(get_user_from_token)],
|
||||
token: Annotated[OAuth2Token | None, Depends(get_token_or_none)],
|
||||
request: Request,
|
||||
resource_id: str | None = None,
|
||||
) -> ProcessResult:
|
||||
"""Generic path for testing a resource provided by a provider"""
|
||||
provider = providers[user.auth_provider_id]
|
||||
# Third party resource (provided through the auth provider)
|
||||
# The token is just passed on
|
||||
if resource_name in [r.resource_name for r in provider.resources]:
|
||||
return await get_auth_provider_resource(
|
||||
provider=provider,
|
||||
resource_name=resource_name,
|
||||
access_token=token["access_token"] if token else None,
|
||||
user=user,
|
||||
)
|
||||
# Internal resource (provided here)
|
||||
if resource_name in registry.resource_providers:
|
||||
resource_provider = registry.resource_providers[resource_name]
|
||||
if resource_provider.scope_required is not None and user.has_scope(
|
||||
resource_provider.scope_required
|
||||
):
|
||||
try:
|
||||
return await resource_provider.process(user=user, resource_id=resource_id)
|
||||
except ProcessError as err:
|
||||
raise HTTPException(
|
||||
status.HTTP_401_UNAUTHORIZED, f"Cannot process resource: {err}"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
f"No scope {resource_provider.scope_required} in the access token "
|
||||
+ "but it is required for accessing this resource",
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Unknown resource {resource_name}")
|
||||
# return await get_resource_(resource_name, user, **request.query_params)
|
||||
|
||||
|
||||
async def get_auth_provider_resource(
|
||||
provider: Provider, resource_name: str, access_token: str | None, user: User
|
||||
) -> ProcessResult:
|
||||
resource = [r for r in provider.resources if r.resource_name == resource_name][0]
|
||||
async with AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
url=provider.url + resource.url,
|
||||
headers={
|
||||
"Content-type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
},
|
||||
)
|
||||
if resp.is_error:
|
||||
raise HTTPException(resp.status_code, f"Cannot fetch resource: {resp.reason_phrase}")
|
||||
# Only a demo, real application would really process the response
|
||||
resp_length = len(resp.text)
|
||||
if resp_length > 1024:
|
||||
return ProcessResult(
|
||||
result={"msg": f"The resource is too long ({resp_length} bytes) to show here"}
|
||||
)
|
||||
else:
|
||||
return ProcessResult(result=resp.json())
|
||||
|
||||
|
||||
# async def get_resource_(resource_id: str, user: User, **kwargs) -> dict:
|
||||
# """
|
||||
# Resource processing: build an informative rely as a simple showcase
|
||||
# """
|
||||
# if resource_id == "petition":
|
||||
# return await sign(user, kwargs["petition_id"])
|
||||
# provider = providers[user.auth_provider_id]
|
||||
# try:
|
||||
# pname = provider.name
|
||||
# except KeyError:
|
||||
# pname = "?"
|
||||
# resp = {
|
||||
# "hello": f"Hi {user.name} from an OAuth resource provider",
|
||||
# "comment": f"I received a request for '{resource_id}' "
|
||||
# + f"with an access token signed by {pname}",
|
||||
# }
|
||||
# # For the demo, resource resource_id matches a scope get:resource_id,
|
||||
# # but this has to be refined for production
|
||||
# required_scope = f"get:{resource_id}"
|
||||
# # Check if the required scope is in the scopes allowed in userinfo
|
||||
# try:
|
||||
# if user.has_scope(required_scope):
|
||||
# await process(user, resource_id, resp)
|
||||
# else:
|
||||
# ## For the showcase, giving a explanation.
|
||||
# ## Alternatively, raise HTTP_401_UNAUTHORIZED
|
||||
# raise HTTPException(
|
||||
# status.HTTP_401_UNAUTHORIZED,
|
||||
# f"No scope {required_scope} in the access token "
|
||||
# + "but it is required for accessing this resource",
|
||||
# )
|
||||
# except ExpiredSignatureError:
|
||||
# raise HTTPException(status.HTTP_401_UNAUTHORIZED, "The token's signature has expired")
|
||||
# except InvalidTokenError:
|
||||
# raise HTTPException(status.HTTP_401_UNAUTHORIZED, "The token is invalid")
|
||||
# return resp
|
||||
|
||||
|
||||
# async def process(user, resource_id, resp):
|
||||
# """
|
||||
# Too simple to be serious.
|
||||
# It's a good fit for a plugin architecture for production
|
||||
# """
|
||||
# if resource_id == "time":
|
||||
# resp["time"] = datetime.now().strftime("%c")
|
||||
# elif resource_id == "bs":
|
||||
# async with AsyncClient() as client:
|
||||
# bs = await client.get("https://corporatebs-generator.sameerkumar.website/")
|
||||
# resp["bs"] = bs.json().get("phrase", "Sorry, i am out of BS today.")
|
||||
# else:
|
||||
# raise HTTPException(
|
||||
# status.HTTP_401_UNAUTHORIZED, f"I don't known how to give '{resource_id}'."
|
||||
# )
|
||||
|
||||
|
||||
# @resource_server.get("/introspect")
|
||||
# async def get_introspect(
|
||||
# request: Request,
|
||||
|
@ -114,99 +236,6 @@ async def get_protected_by_foorole_or_barrole(
|
|||
# else:
|
||||
# raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
|
||||
|
||||
@resource_server.get("/{id}")
|
||||
async def get_resource(
|
||||
id: str,
|
||||
user: Annotated[User, Depends(get_user_from_token)],
|
||||
token: Annotated[OAuth2Token | None, Depends(get_token_or_none)],
|
||||
) -> dict | list:
|
||||
"""Generic path for testing a resource provided by a provider"""
|
||||
provider = providers[user.auth_provider_id]
|
||||
if id in [r.id for r in provider.resources]:
|
||||
return await get_external_resource(
|
||||
provider=provider,
|
||||
id=id,
|
||||
access_token=token["access_token"] if token else None,
|
||||
user=user,
|
||||
)
|
||||
return await get_resource_(id, user)
|
||||
|
||||
|
||||
async def get_external_resource(
|
||||
provider: Provider, id: str, access_token: str | None, user: User
|
||||
) -> dict | list:
|
||||
resource = [r for r in provider.resources if r.id == id][0]
|
||||
async with AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
url=provider.url + resource.url,
|
||||
headers={
|
||||
"Content-type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
},
|
||||
)
|
||||
if resp.is_error:
|
||||
raise HTTPException(resp.status_code, f"Cannot fetch resource: {resp.reason_phrase}")
|
||||
resp_length = len(resp.text)
|
||||
if resp_length > 1024:
|
||||
return {"msg": f"The resource is too long ({resp_length} bytes) to show here"}
|
||||
else:
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def get_resource_(resource_id: str, user: User) -> dict:
|
||||
"""
|
||||
Resource processing: build an informative rely as a simple showcase
|
||||
"""
|
||||
provider = providers[user.auth_provider_id]
|
||||
try:
|
||||
pname = provider.name
|
||||
except KeyError:
|
||||
pname = "?"
|
||||
resp = {
|
||||
"hello": f"Hi {user.name} from an OAuth resource provider",
|
||||
"comment": f"I received a request for '{resource_id}' "
|
||||
+ f"with an access token signed by {pname}",
|
||||
}
|
||||
# For the demo, resource resource_id matches a scope get:resource_id,
|
||||
# but this has to be refined for production
|
||||
required_scope = f"get:{resource_id}"
|
||||
# Check if the required scope is in the scopes allowed in userinfo
|
||||
try:
|
||||
if user.has_scope(required_scope):
|
||||
await process(user, resource_id, resp)
|
||||
else:
|
||||
## For the showcase, giving a explanation.
|
||||
## Alternatively, raise HTTP_401_UNAUTHORIZED
|
||||
raise HTTPException(
|
||||
status.HTTP_401_UNAUTHORIZED,
|
||||
f"No scope {required_scope} in the access token "
|
||||
+ "but it is required for accessing this resource",
|
||||
)
|
||||
except ExpiredSignatureError:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "The token's signature has expired")
|
||||
except InvalidTokenError:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "The token is invalid")
|
||||
return resp
|
||||
|
||||
|
||||
async def process(user, resource_id, resp):
|
||||
"""
|
||||
Too simple to be serious.
|
||||
It's a good fit for a plugin architecture for production
|
||||
"""
|
||||
if resource_id == "time":
|
||||
resp["time"] = datetime.now().strftime("%c")
|
||||
elif resource_id == "bs":
|
||||
async with AsyncClient() as client:
|
||||
bs = await client.get("https://corporatebs-generator.sameerkumar.website/")
|
||||
resp["bs"] = bs.json().get("phrase", "Sorry, i am out of BS today.")
|
||||
else:
|
||||
raise HTTPException(
|
||||
status.HTTP_401_UNAUTHORIZED, f"I don't known how to give '{resource_id}'."
|
||||
)
|
||||
|
||||
|
||||
# assert user.oidc_provider is not None
|
||||
### Get some info (TODO: refactor)
|
||||
# if (auth_provider_id := user.oidc_provider.name) is None:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue