enable policies
This commit is contained in:
@@ -16,6 +16,7 @@ from gatehouse_app.services.oidc_service import (
|
||||
OIDCService, InvalidClientError, InvalidGrantError, InvalidRequestError
|
||||
)
|
||||
from gatehouse_app.services.auth_service import AuthService
|
||||
from gatehouse_app.services.mfa_policy_service import MfaPolicyService
|
||||
from gatehouse_app.extensions import db
|
||||
from gatehouse_app.extensions import bcrypt as flask_bcrypt
|
||||
from gatehouse_app.models import User, OIDCClient
|
||||
@@ -372,6 +373,23 @@ def oidc_authorize():
|
||||
logger.debug("[OIDC] Attempting user authentication for email: %s", email)
|
||||
try:
|
||||
user = AuthService.authenticate(email, password)
|
||||
|
||||
# Evaluate MFA policy after primary authentication
|
||||
policy_result = MfaPolicyService.after_primary_auth_success(user, remember_me=False)
|
||||
|
||||
# Check if user can create full session
|
||||
if not policy_result.can_create_full_session:
|
||||
logger.debug("[OIDC] User cannot create full session due to MFA compliance: user_id=%s, email=%s", user.id, email)
|
||||
return _show_login_page(
|
||||
client_id=client_id,
|
||||
redirect_uri=redirect_uri,
|
||||
scope=scope,
|
||||
state=state,
|
||||
nonce=nonce,
|
||||
response_type=response_type,
|
||||
error="Your account requires multi factor enrollment before using single sign on"
|
||||
)
|
||||
|
||||
user_id = user.id
|
||||
session["oidc_user_id"] = user_id
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ from flask import Blueprint
|
||||
api_v1_bp = Blueprint("api_v1", __name__)
|
||||
|
||||
# Import route modules to register them
|
||||
from gatehouse_app.api.v1 import auth, users, organizations
|
||||
from gatehouse_app.api.v1 import auth, users, organizations, policies
|
||||
|
||||
+133
-24
@@ -22,6 +22,7 @@ from gatehouse_app.schemas.webauthn_schema import (
|
||||
from gatehouse_app.services.auth_service import AuthService
|
||||
from gatehouse_app.services.webauthn_service import WebAuthnService
|
||||
from gatehouse_app.services.user_service import UserService
|
||||
from gatehouse_app.services.mfa_policy_service import MfaPolicyService
|
||||
from gatehouse_app.utils.decorators import login_required
|
||||
from gatehouse_app.utils.constants import AuditAction
|
||||
from gatehouse_app.exceptions.auth_exceptions import InvalidCredentialsError
|
||||
@@ -94,6 +95,9 @@ def login():
|
||||
400: Validation error
|
||||
401: Invalid credentials
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# Validate request data
|
||||
schema = LoginSchema()
|
||||
@@ -105,6 +109,11 @@ def login():
|
||||
password=data["password"],
|
||||
)
|
||||
|
||||
# SECURITY CHECK: Log MFA enrollment status to validate the vulnerability
|
||||
has_totp = user.has_totp_enabled()
|
||||
has_webauthn = user.has_webauthn_enabled()
|
||||
logger.warning(f"[SECURITY DIAGNOSTIC] Login attempt for user {user.email} - TOTP enabled: {has_totp}, WebAuthn enabled: {has_webauthn}")
|
||||
|
||||
# Check if user has TOTP enabled for two-factor authentication
|
||||
if user.has_totp_enabled():
|
||||
# TOTP is enabled - store user_id in session for TOTP verification
|
||||
@@ -121,16 +130,56 @@ def login():
|
||||
)
|
||||
|
||||
# TOTP is NOT enabled - proceed with normal login flow
|
||||
# SECURITY DIAGNOSTIC: This is where the vulnerability occurs - no WebAuthn check!
|
||||
if has_webauthn:
|
||||
logger.error(f"[SECURITY VULNERABILITY DETECTED] User {user.email} has WebAuthn enrolled but is bypassing it! Creating session without MFA verification.")
|
||||
|
||||
# Evaluate MFA policy after primary authentication
|
||||
remember_me = data.get("remember_me", False)
|
||||
policy_result = MfaPolicyService.after_primary_auth_success(user, remember_me)
|
||||
|
||||
# Create session with appropriate duration based on remember_me preference
|
||||
duration = 2592000 if data.get("remember_me") else 86400 # 30 days vs 1 day
|
||||
user_session = AuthService.create_session(user, duration_seconds=duration)
|
||||
duration = 2592000 if remember_me else 86400 # 30 days vs 1 day
|
||||
|
||||
# Determine if this should be a compliance-only session
|
||||
is_compliance_only = policy_result.create_compliance_only_session
|
||||
|
||||
user_session = AuthService.create_session(
|
||||
user,
|
||||
duration_seconds=duration,
|
||||
is_compliance_only=is_compliance_only
|
||||
)
|
||||
|
||||
# Build response data
|
||||
response_data = {
|
||||
"user": user.to_dict(),
|
||||
"token": user_session.token,
|
||||
"expires_at": user_session.expires_at.isoformat() + "Z" if user_session.expires_at.isoformat()[-1] != "Z" else user_session.expires_at.isoformat(),
|
||||
}
|
||||
|
||||
# Add MFA compliance information
|
||||
if policy_result.compliance_summary:
|
||||
response_data["mfa_compliance"] = {
|
||||
"overall_status": policy_result.compliance_summary.overall_status,
|
||||
"missing_methods": policy_result.compliance_summary.missing_methods,
|
||||
"deadline_at": policy_result.compliance_summary.deadline_at,
|
||||
"orgs": [
|
||||
{
|
||||
"organization_id": org.organization_id,
|
||||
"organization_name": org.organization_name,
|
||||
"status": org.status,
|
||||
"deadline_at": org.deadline_at,
|
||||
}
|
||||
for org in policy_result.compliance_summary.orgs
|
||||
],
|
||||
}
|
||||
|
||||
# Add requires_mfa_enrollment flag if compliance-only session
|
||||
if is_compliance_only:
|
||||
response_data["requires_mfa_enrollment"] = True
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"user": user.to_dict(),
|
||||
"token": user_session.token,
|
||||
"expires_at": user_session.expires_at.isoformat() + "Z" if user_session.expires_at.isoformat()[-1] != "Z" else user_session.expires_at.isoformat(),
|
||||
},
|
||||
data=response_data,
|
||||
message="Login successful",
|
||||
)
|
||||
|
||||
@@ -380,20 +429,50 @@ def verify_totp():
|
||||
client_utc_timestamp=data.get("client_timestamp"),
|
||||
)
|
||||
|
||||
# Create full session
|
||||
user_session = AuthService.create_session(user)
|
||||
# Evaluate MFA policy after primary authentication
|
||||
policy_result = MfaPolicyService.after_primary_auth_success(user, remember_me=False)
|
||||
|
||||
# Determine if this should be a compliance-only session
|
||||
is_compliance_only = policy_result.create_compliance_only_session
|
||||
|
||||
# Create session
|
||||
user_session = AuthService.create_session(user, is_compliance_only=is_compliance_only)
|
||||
|
||||
# Clear temporary session
|
||||
session.pop("totp_pending_user_id", None)
|
||||
|
||||
# Build response data
|
||||
response_data = {
|
||||
"user": user.to_dict(),
|
||||
"token": user_session.token,
|
||||
"expires_at": user_session.expires_at.isoformat() + "Z"
|
||||
if user_session.expires_at.isoformat()[-1] != "Z"
|
||||
else user_session.expires_at.isoformat(),
|
||||
}
|
||||
|
||||
# Add MFA compliance information
|
||||
if policy_result.compliance_summary:
|
||||
response_data["mfa_compliance"] = {
|
||||
"overall_status": policy_result.compliance_summary.overall_status,
|
||||
"missing_methods": policy_result.compliance_summary.missing_methods,
|
||||
"deadline_at": policy_result.compliance_summary.deadline_at,
|
||||
"orgs": [
|
||||
{
|
||||
"organization_id": org.organization_id,
|
||||
"organization_name": org.organization_name,
|
||||
"status": org.status,
|
||||
"deadline_at": org.deadline_at,
|
||||
}
|
||||
for org in policy_result.compliance_summary.orgs
|
||||
],
|
||||
}
|
||||
|
||||
# Add requires_mfa_enrollment flag if compliance-only session
|
||||
if is_compliance_only:
|
||||
response_data["requires_mfa_enrollment"] = True
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"user": user.to_dict(),
|
||||
"token": user_session.token,
|
||||
"expires_at": user_session.expires_at.isoformat() + "Z"
|
||||
if user_session.expires_at.isoformat()[-1] != "Z"
|
||||
else user_session.expires_at.isoformat(),
|
||||
},
|
||||
data=response_data,
|
||||
message="TOTP verification successful",
|
||||
)
|
||||
|
||||
@@ -835,22 +914,52 @@ def complete_webauthn_login():
|
||||
challenge
|
||||
)
|
||||
|
||||
# Evaluate MFA policy after primary authentication
|
||||
policy_result = MfaPolicyService.after_primary_auth_success(user, remember_me=False)
|
||||
|
||||
# Determine if this should be a compliance-only session
|
||||
is_compliance_only = policy_result.create_compliance_only_session
|
||||
|
||||
# Create session
|
||||
user_session = AuthService.create_session(user)
|
||||
user_session = AuthService.create_session(user, is_compliance_only=is_compliance_only)
|
||||
|
||||
# Clear pending session
|
||||
session.pop("webauthn_pending_user_id", None)
|
||||
|
||||
logger.info(f"WebAuthn login completed successfully for user: {user.email}")
|
||||
|
||||
# Build response data
|
||||
response_data = {
|
||||
"user": user.to_dict(),
|
||||
"token": user_session.token,
|
||||
"expires_at": user_session.expires_at.isoformat() + "Z"
|
||||
if user_session.expires_at.isoformat()[-1] != "Z"
|
||||
else user_session.expires_at.isoformat(),
|
||||
}
|
||||
|
||||
# Add MFA compliance information
|
||||
if policy_result.compliance_summary:
|
||||
response_data["mfa_compliance"] = {
|
||||
"overall_status": policy_result.compliance_summary.overall_status,
|
||||
"missing_methods": policy_result.compliance_summary.missing_methods,
|
||||
"deadline_at": policy_result.compliance_summary.deadline_at,
|
||||
"orgs": [
|
||||
{
|
||||
"organization_id": org.organization_id,
|
||||
"organization_name": org.organization_name,
|
||||
"status": org.status,
|
||||
"deadline_at": org.deadline_at,
|
||||
}
|
||||
for org in policy_result.compliance_summary.orgs
|
||||
],
|
||||
}
|
||||
|
||||
# Add requires_mfa_enrollment flag if compliance-only session
|
||||
if is_compliance_only:
|
||||
response_data["requires_mfa_enrollment"] = True
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"user": user.to_dict(),
|
||||
"token": user_session.token,
|
||||
"expires_at": user_session.expires_at.isoformat() + "Z"
|
||||
if user_session.expires_at.isoformat()[-1] != "Z"
|
||||
else user_session.expires_at.isoformat(),
|
||||
},
|
||||
data=response_data,
|
||||
message="Login successful",
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from flask import g, request
|
||||
from marshmallow import ValidationError
|
||||
from gatehouse_app.api.v1 import api_v1_bp
|
||||
from gatehouse_app.utils.response import api_response
|
||||
from gatehouse_app.utils.decorators import login_required, require_admin, require_owner
|
||||
from gatehouse_app.utils.decorators import login_required, require_admin, require_owner, full_access_required
|
||||
from gatehouse_app.schemas.organization_schema import (
|
||||
OrganizationCreateSchema,
|
||||
OrganizationUpdateSchema,
|
||||
@@ -17,6 +17,7 @@ from gatehouse_app.utils.constants import OrganizationRole
|
||||
|
||||
@api_v1_bp.route("/organizations", methods=["POST"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def create_organization():
|
||||
"""
|
||||
Create a new organization.
|
||||
@@ -65,6 +66,7 @@ def create_organization():
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>", methods=["GET"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def get_organization(org_id):
|
||||
"""
|
||||
Get organization by ID.
|
||||
@@ -101,6 +103,7 @@ def get_organization(org_id):
|
||||
@api_v1_bp.route("/organizations/<org_id>", methods=["PATCH"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def update_organization(org_id):
|
||||
"""
|
||||
Update organization.
|
||||
@@ -152,6 +155,7 @@ def update_organization(org_id):
|
||||
@api_v1_bp.route("/organizations/<org_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@require_owner
|
||||
@full_access_required
|
||||
def delete_organization(org_id):
|
||||
"""
|
||||
Delete organization (soft delete).
|
||||
@@ -180,6 +184,7 @@ def delete_organization(org_id):
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>/members", methods=["GET"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def get_organization_members(org_id):
|
||||
"""
|
||||
Get all members of an organization.
|
||||
@@ -223,6 +228,7 @@ def get_organization_members(org_id):
|
||||
@api_v1_bp.route("/organizations/<org_id>/members", methods=["POST"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def add_organization_member(org_id):
|
||||
"""
|
||||
Add a member to the organization.
|
||||
@@ -290,6 +296,7 @@ def add_organization_member(org_id):
|
||||
@api_v1_bp.route("/organizations/<org_id>/members/<user_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def remove_organization_member(org_id, user_id):
|
||||
"""
|
||||
Remove a member from the organization.
|
||||
@@ -320,6 +327,7 @@ def remove_organization_member(org_id, user_id):
|
||||
@api_v1_bp.route("/organizations/<org_id>/members/<user_id>/role", methods=["PATCH"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def update_member_role(org_id, user_id):
|
||||
"""
|
||||
Update a member's role.
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
"""Security policy endpoints."""
|
||||
from flask import g, request
|
||||
from marshmallow import Schema, fields, validate, ValidationError
|
||||
from gatehouse_app.api.v1 import api_v1_bp
|
||||
from gatehouse_app.utils.response import api_response
|
||||
from gatehouse_app.utils.decorators import login_required, require_admin, full_access_required
|
||||
from gatehouse_app.services.mfa_policy_service import MfaPolicyService
|
||||
from gatehouse_app.services.organization_service import OrganizationService
|
||||
from gatehouse_app.services.audit_service import AuditService
|
||||
from gatehouse_app.utils.constants import MfaPolicyMode, MfaRequirementOverride, MfaComplianceStatus, AuditAction
|
||||
|
||||
|
||||
class UpdateOrgPolicySchema(Schema):
|
||||
"""Schema for updating organization security policy."""
|
||||
mfa_policy_mode = fields.String(
|
||||
required=False,
|
||||
validate=validate.OneOf([m.value for m in MfaPolicyMode])
|
||||
)
|
||||
mfa_grace_period_days = fields.Integer(
|
||||
required=False,
|
||||
validate=validate.Range(min=0, max=365)
|
||||
)
|
||||
notify_days_before = fields.Integer(
|
||||
required=False,
|
||||
validate=validate.Range(min=0, max=30)
|
||||
)
|
||||
|
||||
|
||||
class UpdateUserPolicySchema(Schema):
|
||||
"""Schema for updating user security policy override."""
|
||||
mfa_override_mode = fields.String(
|
||||
required=True,
|
||||
validate=validate.OneOf([m.value for m in MfaRequirementOverride])
|
||||
)
|
||||
force_totp = fields.Boolean(required=False, load_default=False)
|
||||
force_webauthn = fields.Boolean(required=False, load_default=False)
|
||||
|
||||
|
||||
class ComplianceListQuerySchema(Schema):
|
||||
"""Schema for compliance list query parameters."""
|
||||
status = fields.String(required=False)
|
||||
limit = fields.Integer(required=False, load_default=100)
|
||||
offset = fields.Integer(required=False, load_default=0)
|
||||
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>/security-policy", methods=["GET"])
|
||||
@login_required
|
||||
def get_org_security_policy(org_id):
|
||||
"""
|
||||
Get organization security policy.
|
||||
|
||||
Args:
|
||||
org_id: Organization ID
|
||||
|
||||
Returns:
|
||||
200: Organization security policy
|
||||
401: Not authenticated
|
||||
403: Not a member
|
||||
404: Organization not found
|
||||
"""
|
||||
org = OrganizationService.get_organization_by_id(org_id)
|
||||
|
||||
# Check if user is a member
|
||||
if not org.is_member(g.current_user.id):
|
||||
return api_response(
|
||||
success=False,
|
||||
message="You are not a member of this organization",
|
||||
status=403,
|
||||
error_type="AUTHORIZATION_ERROR",
|
||||
)
|
||||
|
||||
policy_dto = MfaPolicyService.get_org_policy(org_id)
|
||||
|
||||
if policy_dto:
|
||||
data = {
|
||||
"organization_id": policy_dto.organization_id,
|
||||
"mfa_policy_mode": policy_dto.mfa_policy_mode,
|
||||
"mfa_grace_period_days": policy_dto.mfa_grace_period_days,
|
||||
"notify_days_before": policy_dto.notify_days_before,
|
||||
"policy_version": policy_dto.policy_version,
|
||||
}
|
||||
else:
|
||||
# Return default policy if none exists
|
||||
data = {
|
||||
"organization_id": org_id,
|
||||
"mfa_policy_mode": MfaPolicyMode.OPTIONAL.value,
|
||||
"mfa_grace_period_days": 14,
|
||||
"notify_days_before": 7,
|
||||
"policy_version": 0,
|
||||
}
|
||||
|
||||
return api_response(
|
||||
data={"security_policy": data},
|
||||
message="Security policy retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>/security-policy", methods=["PUT"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def update_org_security_policy(org_id):
|
||||
"""
|
||||
Update organization security policy.
|
||||
|
||||
Args:
|
||||
org_id: Organization ID
|
||||
|
||||
Request body:
|
||||
mfa_policy_mode: MFA policy mode (disabled, optional, require_totp, require_webauthn, require_totp_or_webauthn)
|
||||
mfa_grace_period_days: Grace period in days (0-365)
|
||||
notify_days_before: Days before deadline to notify (0-30)
|
||||
|
||||
Returns:
|
||||
200: Security policy updated successfully
|
||||
400: Validation error
|
||||
401: Not authenticated
|
||||
403: Not an admin
|
||||
404: Organization not found
|
||||
"""
|
||||
try:
|
||||
schema = UpdateOrgPolicySchema()
|
||||
data = schema.load(request.json)
|
||||
|
||||
org = OrganizationService.get_organization_by_id(org_id)
|
||||
|
||||
# Update policy
|
||||
policy = MfaPolicyService.create_org_policy(
|
||||
organization_id=org_id,
|
||||
mfa_policy_mode=MfaPolicyMode(data.get("mfa_policy_mode", MfaPolicyMode.OPTIONAL.value)),
|
||||
mfa_grace_period_days=data.get("mfa_grace_period_days", 14),
|
||||
notify_days_before=data.get("notify_days_before", 7),
|
||||
updated_by_user_id=g.current_user.id,
|
||||
)
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"security_policy": {
|
||||
"organization_id": policy.organization_id,
|
||||
"mfa_policy_mode": policy.mfa_policy_mode.value,
|
||||
"mfa_grace_period_days": policy.mfa_grace_period_days,
|
||||
"notify_days_before": policy.notify_days_before,
|
||||
"policy_version": policy.policy_version,
|
||||
}
|
||||
},
|
||||
message="Security policy updated successfully",
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Validation failed",
|
||||
status=400,
|
||||
error_type="VALIDATION_ERROR",
|
||||
error_details=e.messages,
|
||||
)
|
||||
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>/mfa-compliance", methods=["GET"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def get_org_mfa_compliance(org_id):
|
||||
"""
|
||||
Get MFA compliance list for an organization.
|
||||
|
||||
Args:
|
||||
org_id: Organization ID
|
||||
|
||||
Query params:
|
||||
status: Optional status filter (not_applicable, pending, in_grace, compliant, past_due, suspended)
|
||||
limit: Maximum records to return (default 100)
|
||||
offset: Offset for pagination (default 0)
|
||||
|
||||
Returns:
|
||||
200: List of compliance records
|
||||
401: Not authenticated
|
||||
403: Not an admin
|
||||
404: Organization not found
|
||||
"""
|
||||
org = OrganizationService.get_organization_by_id(org_id)
|
||||
|
||||
# Parse query params
|
||||
status = None
|
||||
if request.args.get("status"):
|
||||
try:
|
||||
status = MfaComplianceStatus(request.args.get("status"))
|
||||
except ValueError:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Invalid status value",
|
||||
status=400,
|
||||
error_type="VALIDATION_ERROR",
|
||||
)
|
||||
|
||||
limit = min(int(request.args.get("limit", 100)), 100)
|
||||
offset = int(request.args.get("offset", 0))
|
||||
|
||||
compliance_list = MfaPolicyService.get_org_compliance_list(
|
||||
organization_id=org_id,
|
||||
status=status,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"compliance": compliance_list,
|
||||
"count": len(compliance_list),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
message="Compliance records retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@api_v1_bp.route(
|
||||
"/organizations/<org_id>/users/<user_id>/security-policy", methods=["PATCH"]
|
||||
)
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def update_user_security_policy(org_id, user_id):
|
||||
"""
|
||||
Update user security policy override.
|
||||
|
||||
Args:
|
||||
org_id: Organization ID
|
||||
user_id: User ID
|
||||
|
||||
Request body:
|
||||
mfa_override_mode: Override mode (inherit, required, exempt)
|
||||
force_totp: Force TOTP requirement (default false)
|
||||
force_webauthn: Force WebAuthn requirement (default false)
|
||||
|
||||
Returns:
|
||||
200: User policy updated successfully
|
||||
400: Validation error
|
||||
401: Not authenticated
|
||||
403: Not an admin
|
||||
404: Organization or user not found
|
||||
"""
|
||||
try:
|
||||
schema = UpdateUserPolicySchema()
|
||||
data = schema.load(request.json)
|
||||
|
||||
org = OrganizationService.get_organization_by_id(org_id)
|
||||
|
||||
# Check if user is a member of the organization
|
||||
if not org.is_member(user_id):
|
||||
return api_response(
|
||||
success=False,
|
||||
message="User is not a member of this organization",
|
||||
status=404,
|
||||
error_type="NOT_FOUND",
|
||||
)
|
||||
|
||||
# Update user policy
|
||||
policy = MfaPolicyService.set_user_override(
|
||||
user_id=user_id,
|
||||
organization_id=org_id,
|
||||
mfa_override_mode=MfaRequirementOverride(data["mfa_override_mode"]),
|
||||
force_totp=data.get("force_totp", False),
|
||||
force_webauthn=data.get("force_webauthn", False),
|
||||
updated_by_user_id=g.current_user.id,
|
||||
)
|
||||
|
||||
# Log the override change with details
|
||||
AuditService.log_action(
|
||||
action=AuditAction.USER_SECURITY_POLICY_OVERRIDE_UPDATE,
|
||||
user_id=g.current_user.id,
|
||||
organization_id=org_id,
|
||||
resource_type="user",
|
||||
resource_id=user_id,
|
||||
description=f"User security policy override changed to {data['mfa_override_mode']} for user {user_id}",
|
||||
)
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"user_security_policy": {
|
||||
"user_id": policy.user_id,
|
||||
"organization_id": policy.organization_id,
|
||||
"mfa_override_mode": policy.mfa_override_mode.value,
|
||||
"force_totp": policy.force_totp,
|
||||
"force_webauthn": policy.force_webauthn,
|
||||
}
|
||||
},
|
||||
message="User security policy updated successfully",
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
return api_response(
|
||||
success=False,
|
||||
message="Validation failed",
|
||||
status=400,
|
||||
error_type="VALIDATION_ERROR",
|
||||
error_details=e.messages,
|
||||
)
|
||||
|
||||
|
||||
@api_v1_bp.route("/users/me/mfa-compliance", methods=["GET"])
|
||||
@login_required
|
||||
def get_my_mfa_compliance():
|
||||
"""
|
||||
Get current user's MFA compliance across all organizations.
|
||||
|
||||
Returns:
|
||||
200: MFA compliance summary
|
||||
401: Not authenticated
|
||||
"""
|
||||
user = g.current_user
|
||||
|
||||
compliance_summary = MfaPolicyService.evaluate_user_mfa_state(user)
|
||||
|
||||
orgs = []
|
||||
for org_state in compliance_summary.orgs:
|
||||
orgs.append({
|
||||
"organization_id": org_state.organization_id,
|
||||
"organization_name": org_state.organization_name,
|
||||
"status": org_state.status,
|
||||
"effective_mode": org_state.effective_mode,
|
||||
"deadline_at": org_state.deadline_at,
|
||||
"applied_at": org_state.applied_at,
|
||||
})
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"mfa_compliance": {
|
||||
"overall_status": compliance_summary.overall_status,
|
||||
"missing_methods": compliance_summary.missing_methods,
|
||||
"deadline_at": compliance_summary.deadline_at,
|
||||
"orgs": orgs,
|
||||
}
|
||||
},
|
||||
message="MFA compliance retrieved successfully",
|
||||
)
|
||||
@@ -3,7 +3,7 @@ from flask import g, request
|
||||
from marshmallow import ValidationError
|
||||
from gatehouse_app.api.v1 import api_v1_bp
|
||||
from gatehouse_app.utils.response import api_response
|
||||
from gatehouse_app.utils.decorators import login_required
|
||||
from gatehouse_app.utils.decorators import login_required, full_access_required
|
||||
from gatehouse_app.schemas.user_schema import UserUpdateSchema, ChangePasswordSchema
|
||||
from gatehouse_app.services.user_service import UserService
|
||||
from gatehouse_app.services.auth_service import AuthService
|
||||
@@ -29,6 +29,7 @@ def get_me():
|
||||
|
||||
@api_v1_bp.route("/users/me", methods=["PATCH"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def update_me():
|
||||
"""
|
||||
Update current user profile.
|
||||
@@ -67,6 +68,7 @@ def update_me():
|
||||
|
||||
@api_v1_bp.route("/users/me", methods=["DELETE"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def delete_me():
|
||||
"""
|
||||
Delete current user account (soft delete).
|
||||
@@ -84,6 +86,7 @@ def delete_me():
|
||||
|
||||
@api_v1_bp.route("/users/me/password", methods=["POST"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def change_password():
|
||||
"""
|
||||
Change current user password.
|
||||
@@ -136,6 +139,7 @@ def change_password():
|
||||
|
||||
@api_v1_bp.route("/users/me/organizations", methods=["GET"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
def get_my_organizations():
|
||||
"""
|
||||
Get all organizations current user is a member of.
|
||||
|
||||
Reference in New Issue
Block a user