move app to gatehouse-app
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""Utilities package."""
|
||||
from gatehouse_app.utils.response import api_response
|
||||
from gatehouse_app.utils.constants import (
|
||||
UserStatus,
|
||||
OrganizationRole,
|
||||
AuthMethodType,
|
||||
SessionStatus,
|
||||
AuditAction,
|
||||
ErrorType,
|
||||
)
|
||||
from gatehouse_app.utils.decorators import login_required, require_role, require_owner, require_admin
|
||||
|
||||
__all__ = [
|
||||
"api_response",
|
||||
"UserStatus",
|
||||
"OrganizationRole",
|
||||
"AuthMethodType",
|
||||
"SessionStatus",
|
||||
"AuditAction",
|
||||
"ErrorType",
|
||||
"login_required",
|
||||
"require_role",
|
||||
"require_owner",
|
||||
"require_admin",
|
||||
]
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Application constants and enums."""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class UserStatus(str, Enum):
|
||||
"""User account status."""
|
||||
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
SUSPENDED = "suspended"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class OrganizationRole(str, Enum):
|
||||
"""Organization member roles."""
|
||||
|
||||
OWNER = "owner"
|
||||
ADMIN = "admin"
|
||||
MEMBER = "member"
|
||||
GUEST = "guest"
|
||||
|
||||
|
||||
class AuthMethodType(str, Enum):
|
||||
"""Authentication method types."""
|
||||
|
||||
PASSWORD = "password"
|
||||
TOTP = "totp"
|
||||
GOOGLE = "google"
|
||||
GITHUB = "github"
|
||||
MICROSOFT = "microsoft"
|
||||
SAML = "saml"
|
||||
OIDC = "oidc"
|
||||
WEBAUTHN = "webauthn"
|
||||
|
||||
|
||||
class SessionStatus(str, Enum):
|
||||
"""Session status."""
|
||||
|
||||
ACTIVE = "active"
|
||||
EXPIRED = "expired"
|
||||
REVOKED = "revoked"
|
||||
|
||||
|
||||
class AuditAction(str, Enum):
|
||||
"""Audit log action types."""
|
||||
|
||||
# User actions
|
||||
USER_LOGIN = "user.login"
|
||||
USER_LOGOUT = "user.logout"
|
||||
USER_REGISTER = "user.register"
|
||||
USER_UPDATE = "user.update"
|
||||
USER_DELETE = "user.delete"
|
||||
PASSWORD_CHANGE = "user.password_change"
|
||||
PASSWORD_RESET = "user.password_reset"
|
||||
|
||||
# Organization actions
|
||||
ORG_CREATE = "org.create"
|
||||
ORG_UPDATE = "org.update"
|
||||
ORG_DELETE = "org.delete"
|
||||
ORG_MEMBER_ADD = "org.member.add"
|
||||
ORG_MEMBER_REMOVE = "org.member.remove"
|
||||
ORG_MEMBER_ROLE_CHANGE = "org.member.role_change"
|
||||
|
||||
# Session actions
|
||||
SESSION_CREATE = "session.create"
|
||||
SESSION_REVOKE = "session.revoke"
|
||||
|
||||
# Auth method actions
|
||||
AUTH_METHOD_ADD = "auth.method.add"
|
||||
AUTH_METHOD_REMOVE = "auth.method.remove"
|
||||
TOTP_ENROLL_INITIATED = "totp.enroll.initiated"
|
||||
TOTP_ENROLL_COMPLETED = "totp.enroll.completed"
|
||||
TOTP_VERIFY_SUCCESS = "totp.verify.success"
|
||||
TOTP_VERIFY_FAILED = "totp.verify.failed"
|
||||
TOTP_DISABLED = "totp.disabled"
|
||||
TOTP_BACKUP_CODE_USED = "totp.backup_code.used"
|
||||
TOTP_BACKUP_CODES_REGENERATED = "totp.backup_codes.regenerated"
|
||||
|
||||
# WebAuthn actions
|
||||
WEBAUTHN_REGISTER_INITIATED = "webauthn.register.initiated"
|
||||
WEBAUTHN_REGISTER_COMPLETED = "webauthn.register.completed"
|
||||
WEBAUTHN_REGISTER_FAILED = "webauthn.register.failed"
|
||||
WEBAUTHN_LOGIN_INITIATED = "webauthn.login.initiated"
|
||||
WEBAUTHN_LOGIN_SUCCESS = "webauthn.login.success"
|
||||
WEBAUTHN_LOGIN_FAILED = "webauthn.login.failed"
|
||||
WEBAUTHN_CREDENTIAL_DELETED = "webauthn.credential.deleted"
|
||||
WEBAUTHN_CREDENTIAL_RENAMED = "webauthn.credential.renamed"
|
||||
|
||||
|
||||
class OIDCGrantType(str, Enum):
|
||||
"""OIDC grant types."""
|
||||
|
||||
AUTHORIZATION_CODE = "authorization_code"
|
||||
IMPLICIT = "implicit"
|
||||
REFRESH_TOKEN = "refresh_token"
|
||||
CLIENT_CREDENTIALS = "client_credentials"
|
||||
|
||||
|
||||
class OIDCResponseType(str, Enum):
|
||||
"""OIDC response types."""
|
||||
|
||||
CODE = "code"
|
||||
TOKEN = "token"
|
||||
ID_TOKEN = "id_token"
|
||||
|
||||
|
||||
# Error type constants
|
||||
class ErrorType:
|
||||
"""Error type constants for API responses."""
|
||||
|
||||
VALIDATION_ERROR = "VALIDATION_ERROR"
|
||||
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
|
||||
AUTHORIZATION_ERROR = "AUTHORIZATION_ERROR"
|
||||
NOT_FOUND = "NOT_FOUND"
|
||||
CONFLICT = "CONFLICT"
|
||||
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
||||
INTERNAL_ERROR = "INTERNAL_ERROR"
|
||||
BAD_REQUEST = "BAD_REQUEST"
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Custom decorators for authentication and authorization."""
|
||||
from functools import wraps
|
||||
from flask import request, g
|
||||
from gatehouse_app.utils.response import api_response
|
||||
from gatehouse_app.utils.constants import OrganizationRole
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to require Bearer token authentication.
|
||||
|
||||
Extracts token from Authorization: Bearer {token} header,
|
||||
validates the session, and sets g.current_user and g.current_session.
|
||||
"""
|
||||
from gatehouse_app.services.session_service import SessionService
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Extract token from Authorization header
|
||||
auth_header = request.headers.get('Authorization')
|
||||
|
||||
if not auth_header:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Authorization header is required",
|
||||
status=401,
|
||||
error_type="AUTH_REQUIRED"
|
||||
)
|
||||
|
||||
# Expect format: "Bearer {token}"
|
||||
parts = auth_header.split()
|
||||
if len(parts) != 2 or parts[0].lower() != 'bearer':
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Invalid authorization format. Use: Bearer {token}",
|
||||
status=401,
|
||||
error_type="INVALID_AUTH_FORMAT"
|
||||
)
|
||||
|
||||
token = parts[1]
|
||||
|
||||
# Get active session by token
|
||||
session = SessionService.get_active_session_by_token(token)
|
||||
|
||||
if not session:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Invalid or expired session",
|
||||
status=401,
|
||||
error_type="INVALID_TOKEN"
|
||||
)
|
||||
|
||||
# Validate session is active
|
||||
if not session.is_active():
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Session is no longer active",
|
||||
status=401,
|
||||
error_type="SESSION_INACTIVE"
|
||||
)
|
||||
|
||||
# Update last_activity_at timestamp
|
||||
from datetime import datetime, timezone
|
||||
session.last_activity_at = datetime.now(timezone.utc)
|
||||
from gatehouse_app import db
|
||||
db.session.commit()
|
||||
|
||||
# Set context variables
|
||||
g.current_user = session.user
|
||||
g.current_session = session
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def require_role(*allowed_roles):
|
||||
"""
|
||||
Decorator to require specific organization roles.
|
||||
|
||||
Args:
|
||||
*allowed_roles: Variable number of OrganizationRole values
|
||||
|
||||
Raises:
|
||||
ForbiddenError: If user doesn't have required role
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Ensure user is authenticated first
|
||||
if not hasattr(g, "current_user"):
|
||||
raise UnauthorizedError("Authentication required")
|
||||
|
||||
# Get organization_id from kwargs or URL parameters
|
||||
org_id = kwargs.get("org_id") or kwargs.get("organization_id")
|
||||
if not org_id:
|
||||
raise ForbiddenError("Organization context required")
|
||||
|
||||
# Check user's role in the organization
|
||||
from gatehouse_app.models.organization_member import OrganizationMember
|
||||
|
||||
membership = OrganizationMember.query.filter_by(
|
||||
user_id=g.current_user.id,
|
||||
organization_id=org_id,
|
||||
).first()
|
||||
|
||||
if not membership:
|
||||
raise ForbiddenError("Not a member of this organization")
|
||||
|
||||
if membership.role not in allowed_roles:
|
||||
raise ForbiddenError(
|
||||
f"Requires one of the following roles: {', '.join(allowed_roles)}"
|
||||
)
|
||||
|
||||
g.current_membership = membership
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def require_owner(f):
|
||||
"""Decorator to require organization owner role."""
|
||||
return require_role(OrganizationRole.OWNER)(f)
|
||||
|
||||
|
||||
def require_admin(f):
|
||||
"""Decorator to require organization admin or owner role."""
|
||||
return require_role(OrganizationRole.OWNER, OrganizationRole.ADMIN)(f)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""API response utilities."""
|
||||
from flask import jsonify, g
|
||||
|
||||
|
||||
# Version for the response envelope
|
||||
ENVELOPE_VERSION = "1.0"
|
||||
|
||||
|
||||
def api_response(
|
||||
*,
|
||||
data=None,
|
||||
success=True,
|
||||
message="",
|
||||
status=200,
|
||||
error_type=None,
|
||||
error_details=None,
|
||||
meta=None
|
||||
):
|
||||
"""
|
||||
Create a standardized API response.
|
||||
|
||||
Args:
|
||||
data: Response data (only included if success=True)
|
||||
success: Whether the request was successful
|
||||
message: Human-readable message
|
||||
status: HTTP status code
|
||||
error_type: Type of error (only if success=False)
|
||||
error_details: Additional error details (only if success=False)
|
||||
meta: Additional metadata (pagination, etc.)
|
||||
|
||||
Returns:
|
||||
Tuple of (response, status_code)
|
||||
"""
|
||||
payload = {
|
||||
"version": ENVELOPE_VERSION,
|
||||
"success": success,
|
||||
"code": status,
|
||||
"message": message,
|
||||
"request_id": g.get("request_id", "unknown"),
|
||||
}
|
||||
|
||||
if meta:
|
||||
payload["meta"] = meta
|
||||
|
||||
if success:
|
||||
if data is not None:
|
||||
payload["data"] = data
|
||||
else:
|
||||
payload["error"] = {
|
||||
"type": error_type or "UNKNOWN",
|
||||
"details": error_details or {}
|
||||
}
|
||||
|
||||
return jsonify(payload), status
|
||||
Reference in New Issue
Block a user