-
Notifications
You must be signed in to change notification settings - Fork 7
feat(stac): policy enforcement point proof of concept #569
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
Merged
+959
−81
Merged
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
66d464b
fix: move parse_keycloak_from_openid_url to common/auth package to reuse
botanical a17407f
fix: linting and generalize regexes to use in pep middleware
botanical 921095c
feat: create policy enforcement point for stac api
botanical 2d96128
feat: add pep middleware to stac api
botanical 9d08bef
fix: import common auth in dockerfile
botanical 9e5a724
fix: attempt to resolve dependencies
botanical 0629819
fix: add debugging
botanical 283f9ab
fix: env vars for lambda
botanical 605a881
fix: update to use keycloak secret instead
botanical bb514b4
fix: add prefix to keycloak secret env var
botanical e4c4a9d
fix: update error message, and also update keycloak client to check r…
botanical b0a0342
fix: catch invalid token case and throw helpful error
botanical 38bc05f
fix: move pep middleware to common auth so it's reusable
botanical c64d035
fix: add pep middleware to ingest api, delete old middleware file in …
botanical bec19a6
fix: formatting
botanical 3fa37e1
feat: add integration tests for proof of concept
botanical cd79d46
fix: attempt to fix mutable mapping incompatible type issue
botanical 266818c
fix: attempt to fix tests, and enable transactions on pep int tests
botanical 480d5bb
fix: add logging to conftest to debug
botanical e7c004f
fix: linting
botanical aaf029a
fix: clear cache and reload config and app to properly register endpoint
botanical e4f0113
fix: add root path to pep int tests
botanical e360ced
fix: add back arg comment
botanical a6e4f60
Update common/auth/veda_auth/keycloak_client.py
botanical 5c0b8c1
fix: add path to error message, and add error message function for pe…
botanical b2380c2
fix: add case to handle nonexistent tenant
botanical 9b033e0
fix: attempt to fix tests
botanical 2176ff9
fix: create new error for permission denied
botanical 601fdc1
Merge branch 'develop' into mt-uma/pep-poc
botanical 31c8873
fix: remove return type on check_permission
botanical a8a9a55
fix: formatting, remove unused variable
botanical 6721ff0
fix: update readme
botanical afdc19a
fix: update tests
botanical 243a793
fix: spacing, move import
botanical File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| """Policy Enforcement Point (PEP) middleware""" | ||
|
|
||
| import logging | ||
| import re | ||
| from dataclasses import dataclass | ||
| from typing import Awaitable, Callable, Optional, Sequence | ||
|
|
||
| from veda_auth.keycloak_client import KeycloakPDPClient, TokenError | ||
| from veda_auth.resource_extractors import COLLECTIONS_CREATE_PATH_RE | ||
|
|
||
| from starlette.middleware.base import BaseHTTPMiddleware | ||
| from starlette.requests import Request | ||
| from starlette.responses import JSONResponse, Response | ||
| from starlette.types import ASGIApp | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class ProtectedRoute: | ||
| """Routes protected by policy decision point | ||
|
|
||
| path_re: regex pattern applied to request path | ||
| method: HTTP method | ||
| scope: Keycloak resource scope name to check (e.g. "create", "update", "delete") | ||
| """ | ||
|
|
||
| path_re: str | ||
| method: str | ||
| scope: str | ||
|
|
||
|
|
||
| DEFAULT_PROTECTED_ROUTES: Sequence[ProtectedRoute] = ( | ||
| ProtectedRoute(path_re=COLLECTIONS_CREATE_PATH_RE, method="POST", scope="create"), | ||
| ) | ||
|
|
||
|
|
||
| class PEPMiddleware(BaseHTTPMiddleware): | ||
| """Middleware that enforces UMA authorization""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| app: ASGIApp, | ||
| *, | ||
| pdp_client: Callable[[], KeycloakPDPClient], | ||
| resource_extractor: Callable[[Request], Awaitable[Optional[str]]], | ||
| protected_routes: Optional[Sequence[ProtectedRoute]] = None, | ||
| ): | ||
| """Configure PEP middleware with a PDP client, resource extractor, and protected routes.""" | ||
| super().__init__(app) | ||
| self._get_pdp_client = pdp_client | ||
| self._extract_resource_id = resource_extractor | ||
| routes = ( | ||
| protected_routes | ||
| if protected_routes is not None | ||
| else DEFAULT_PROTECTED_ROUTES | ||
| ) | ||
| self._compiled = [ | ||
| (re.compile(r.path_re), r.method.upper(), r.scope) for r in routes | ||
| ] | ||
|
|
||
| def _get_matching_scope_and_route( | ||
| self, request: Request | ||
| ) -> Optional[tuple[str, str]]: | ||
| """Return (scope, method) for the route that matches, otherwise return None""" | ||
| path = request.url.path.rstrip("/") or "/" | ||
| method = request.method.upper() | ||
| for pattern, route_method, scope in self._compiled: | ||
| if route_method == method and pattern.search(path): | ||
| return (scope, route_method) | ||
| return None | ||
|
|
||
| def _get_bearer_token(self, request: Request) -> Optional[str]: | ||
| """Extract the Bearer token from the Authorization header""" | ||
| auth = request.headers.get("Authorization") | ||
| if not auth or not auth.startswith("Bearer "): | ||
| return None | ||
| return auth[7:].strip() | ||
|
|
||
| async def dispatch(self, request: Request, call_next: Callable) -> Response: | ||
| """Check UMA authorization for protected routes, pass through otherwise.""" | ||
| matched_request = self._get_matching_scope_and_route(request) | ||
| if matched_request is None: | ||
| logger.debug( | ||
| "PEP: no protected route match for %s %s... continuing", | ||
| request.method, | ||
| request.url.path, | ||
| ) | ||
| return await call_next(request) | ||
|
|
||
| scope, _method = matched_request | ||
| logger.info( | ||
| "PEP: matched protected route %s %s and scope=%s", | ||
| _method, | ||
| request.url.path, | ||
| scope, | ||
| ) | ||
|
|
||
| pdp_client = self._get_pdp_client() | ||
|
|
||
| token = self._get_bearer_token(request) | ||
| if not token: | ||
| logger.warning( | ||
| "PEP: missing Bearer token for %s %s", _method, request.url.path | ||
| ) | ||
| return JSONResponse( | ||
| status_code=401, | ||
| content={ | ||
| "detail": "Missing or invalid Authorization header (Bearer token required)" | ||
| }, | ||
| headers={"WWW-Authenticate": "Bearer"}, | ||
| ) | ||
|
|
||
| resource_id = await self._extract_resource_id(request) | ||
| if not resource_id: | ||
| logger.warning("PEP: no resource ID for %s %s", _method, request.url.path) | ||
| return JSONResponse( | ||
| status_code=403, | ||
| content={"detail": "Could not determine resource for authorization"}, | ||
| ) | ||
|
|
||
| logger.info( | ||
| "PEP: checking permission resource_id=%s, scope=%s, path=%s", | ||
| resource_id, | ||
| scope, | ||
| request.url.path, | ||
| ) | ||
|
|
||
| try: | ||
| authorized = pdp_client.check_permission( | ||
| access_token=token, | ||
| resource_id=resource_id, | ||
| scope=scope, | ||
| ) | ||
| except TokenError as e: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Appreciate the structured and comprehensive error responses! |
||
| logger.warning( | ||
| "PEP: token error for %s %s: %s", _method, request.url.path, e.detail | ||
| ) | ||
| return JSONResponse( | ||
| status_code=401, | ||
| content={"detail": e.detail}, | ||
| headers={"WWW-Authenticate": 'Bearer error="invalid_token"'}, | ||
| ) | ||
| except Exception as e: | ||
| logger.exception( | ||
| "PEP: Keycloak check failed for resource_id=%s scope=%s: %s", | ||
| resource_id, | ||
| scope, | ||
| e, | ||
| ) | ||
| return JSONResponse( | ||
| status_code=502, | ||
| content={"detail": "Authorization service temporarily unavailable"}, | ||
| ) | ||
botanical marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| logger.info( | ||
| "PEP: authorization result=%s for resource_id=%s, scope=%s, path=%s", | ||
| authorized, | ||
| resource_id, | ||
| scope, | ||
| request.url.path, | ||
| ) | ||
|
|
||
| if not authorized: | ||
| logger.warning( | ||
| "PEP: denied %s %s resource_id=%s, scope=%s", | ||
| _method, | ||
| request.url.path, | ||
| resource_id, | ||
| scope, | ||
| ) | ||
| return JSONResponse( | ||
| status_code=403, | ||
| content={ | ||
| "detail": ( | ||
| f"You do not have permission to {scope} this resource " | ||
| f"({resource_id}). Verify that your user belongs to " | ||
| f"the required tenant and role needed." | ||
| ) | ||
| }, | ||
| ) | ||
|
|
||
| return await call_next(request) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.