Spaces:
Running
Running
Download core/security.py from DJ-Goanna-Coding/Oppo-TIA: direct link, hf CLI and curl.
- Browser
- Download file 4.42 kB
-
https://huggingface.co/spaces/DJ-Goanna-Coding/Oppo-TIA/resolve/main/core/security.py
- Command line
-
hf download hf://spaces/DJ-Goanna-Coding/Oppo-TIA/core/security.py
-
curl -L -o security.py https://huggingface.co/spaces/DJ-Goanna-Coding/Oppo-TIA/resolve/main/core/security.py
4.42 kB
| """JWT Bearer authentication with lightweight ABAC checks. | |
| The API uses symmetric HS256 JWTs. Tokens carry standard claims plus an | |
| ``attrs`` object used for attribute-based access control, e.g.: | |
| {"sub": "alice", "exp": 1700000000, | |
| "attrs": {"role": "admin", "scopes": ["harvest:read", "harvest:write"]}} | |
| Configuration via environment: | |
| JWT_SECRET required HMAC secret for signing / verifying tokens | |
| JWT_ALGORITHM optional, defaults to HS256 | |
| JWT_ISSUER optional issuer claim to validate | |
| JWT_AUDIENCE optional audience claim to validate | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from dataclasses import dataclass | |
| import jwt | |
| from fastapi import Depends, HTTPException, status | |
| from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer | |
| _bearer_scheme = HTTPBearer(auto_error=False) | |
| class Principal: | |
| """Authenticated caller and their attributes.""" | |
| subject: str | |
| attrs: dict | |
| def has_scope(self, scope: str) -> bool: | |
| scopes = self.attrs.get("scopes") or [] | |
| return scope in scopes | |
| def has_role(self, role: str) -> bool: | |
| return self.attrs.get("role") == role | |
| def _get_secret() -> str: | |
| secret = os.environ.get("JWT_SECRET") | |
| if not secret: | |
| # Fail closed — never accept tokens without a configured secret. | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="JWT_SECRET is not configured", | |
| ) | |
| return secret | |
| def _decode(token: str) -> dict: | |
| algorithm = os.environ.get("JWT_ALGORITHM", "HS256") | |
| options = {"require": ["exp"]} | |
| kwargs: dict = {"algorithms": [algorithm], "options": options} | |
| audience = os.environ.get("JWT_AUDIENCE") | |
| if audience: | |
| kwargs["audience"] = audience | |
| issuer = os.environ.get("JWT_ISSUER") | |
| if issuer: | |
| kwargs["issuer"] = issuer | |
| try: | |
| return jwt.decode(token, _get_secret(), **kwargs) | |
| except jwt.ExpiredSignatureError as exc: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Token has expired", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) from exc | |
| except jwt.InvalidTokenError as exc: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Invalid authentication token", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) from exc | |
| def get_current_principal( | |
| credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme), | |
| ) -> Principal: | |
| """FastAPI dependency that resolves the caller from a Bearer token.""" | |
| if credentials is None or credentials.scheme.lower() != "bearer": | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Missing Bearer credentials", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| payload = _decode(credentials.credentials) | |
| subject = payload.get("sub") or payload.get("subject") | |
| if not subject: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Token is missing 'sub' claim", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| attrs = payload.get("attrs") or {} | |
| if not isinstance(attrs, dict): | |
| attrs = {} | |
| # Fall back to top-level scope/role if not nested under attrs. | |
| if "scopes" not in attrs and "scope" in payload: | |
| scope = payload["scope"] | |
| attrs["scopes"] = scope.split() if isinstance(scope, str) else list(scope) | |
| if "role" not in attrs and "role" in payload: | |
| attrs["role"] = payload["role"] | |
| return Principal(subject=str(subject), attrs=attrs) | |
| def require_scopes(*required: str): | |
| """Return a dependency enforcing that the caller has all given scopes.""" | |
| required_set = tuple(required) | |
| def _checker(principal: Principal = Depends(get_current_principal)) -> Principal: | |
| missing: list[str] = [s for s in required_set if not principal.has_scope(s)] | |
| if missing: | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail=f"Missing required scopes: {', '.join(required_set)}", | |
| ) | |
| return principal | |
| return _checker | |