kekkai-cli 1.0.5__py3-none-any.whl → 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. kekkai/cli.py +789 -19
  2. kekkai/compliance/__init__.py +68 -0
  3. kekkai/compliance/hipaa.py +235 -0
  4. kekkai/compliance/mappings.py +136 -0
  5. kekkai/compliance/owasp.py +517 -0
  6. kekkai/compliance/owasp_agentic.py +267 -0
  7. kekkai/compliance/pci_dss.py +205 -0
  8. kekkai/compliance/soc2.py +209 -0
  9. kekkai/dojo.py +91 -14
  10. kekkai/dojo_import.py +9 -1
  11. kekkai/fix/__init__.py +47 -0
  12. kekkai/fix/audit.py +278 -0
  13. kekkai/fix/differ.py +427 -0
  14. kekkai/fix/engine.py +500 -0
  15. kekkai/fix/prompts.py +251 -0
  16. kekkai/output.py +10 -12
  17. kekkai/report/__init__.py +41 -0
  18. kekkai/report/compliance_matrix.py +98 -0
  19. kekkai/report/generator.py +365 -0
  20. kekkai/report/html.py +69 -0
  21. kekkai/report/pdf.py +63 -0
  22. kekkai/report/unified.py +226 -0
  23. kekkai/scanners/container.py +33 -3
  24. kekkai/scanners/gitleaks.py +3 -1
  25. kekkai/scanners/semgrep.py +1 -1
  26. kekkai/scanners/trivy.py +1 -1
  27. kekkai/threatflow/model_adapter.py +143 -1
  28. kekkai/triage/__init__.py +54 -1
  29. kekkai/triage/loader.py +196 -0
  30. kekkai_cli-1.1.1.dist-info/METADATA +379 -0
  31. {kekkai_cli-1.0.5.dist-info → kekkai_cli-1.1.1.dist-info}/RECORD +34 -33
  32. {kekkai_cli-1.0.5.dist-info → kekkai_cli-1.1.1.dist-info}/entry_points.txt +0 -1
  33. {kekkai_cli-1.0.5.dist-info → kekkai_cli-1.1.1.dist-info}/top_level.txt +0 -1
  34. kekkai_cli-1.0.5.dist-info/METADATA +0 -135
  35. portal/__init__.py +0 -19
  36. portal/api.py +0 -155
  37. portal/auth.py +0 -103
  38. portal/enterprise/__init__.py +0 -32
  39. portal/enterprise/audit.py +0 -435
  40. portal/enterprise/licensing.py +0 -342
  41. portal/enterprise/rbac.py +0 -276
  42. portal/enterprise/saml.py +0 -595
  43. portal/ops/__init__.py +0 -53
  44. portal/ops/backup.py +0 -553
  45. portal/ops/log_shipper.py +0 -469
  46. portal/ops/monitoring.py +0 -517
  47. portal/ops/restore.py +0 -469
  48. portal/ops/secrets.py +0 -408
  49. portal/ops/upgrade.py +0 -591
  50. portal/tenants.py +0 -340
  51. portal/uploads.py +0 -259
  52. portal/web.py +0 -384
  53. {kekkai_cli-1.0.5.dist-info → kekkai_cli-1.1.1.dist-info}/WHEEL +0 -0
@@ -1,342 +0,0 @@
1
- """Enterprise license validation and feature gating.
2
-
3
- Security controls:
4
- - Server-side license enforcement
5
- - Signed license tokens to prevent tampering
6
- - Grace period handling for expiration
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import base64
12
- import hashlib
13
- import hmac
14
- import json
15
- import logging
16
- import time
17
- from dataclasses import dataclass, field
18
- from datetime import UTC, datetime, timedelta
19
- from enum import Enum
20
- from typing import TYPE_CHECKING, Any
21
-
22
- if TYPE_CHECKING:
23
- pass
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- GRACE_PERIOD_DAYS = 7
28
- LICENSE_CHECK_INTERVAL = 3600
29
-
30
-
31
- class LicenseStatus(Enum):
32
- """License validation status."""
33
-
34
- VALID = "valid"
35
- EXPIRED = "expired"
36
- GRACE_PERIOD = "grace_period"
37
- INVALID = "invalid"
38
- MISSING = "missing"
39
-
40
-
41
- class LicenseTier(Enum):
42
- """License tier levels."""
43
-
44
- COMMUNITY = "community"
45
- PROFESSIONAL = "professional"
46
- ENTERPRISE = "enterprise"
47
-
48
-
49
- class EnterpriseFeature(Enum):
50
- """Features available in enterprise tier."""
51
-
52
- SSO_SAML = "sso_saml"
53
- RBAC = "rbac"
54
- AUDIT_LOGGING = "audit_logging"
55
- CUSTOM_BRANDING = "custom_branding"
56
- API_RATE_LIMIT_INCREASE = "api_rate_limit_increase"
57
- PRIORITY_SUPPORT = "priority_support"
58
- SLA_GUARANTEE = "sla_guarantee"
59
- MULTI_REGION = "multi_region"
60
- ADVANCED_REPORTS = "advanced_reports"
61
-
62
-
63
- TIER_FEATURES: dict[LicenseTier, frozenset[EnterpriseFeature]] = {
64
- LicenseTier.COMMUNITY: frozenset(),
65
- LicenseTier.PROFESSIONAL: frozenset(
66
- {
67
- EnterpriseFeature.AUDIT_LOGGING,
68
- EnterpriseFeature.API_RATE_LIMIT_INCREASE,
69
- }
70
- ),
71
- LicenseTier.ENTERPRISE: frozenset(
72
- {
73
- EnterpriseFeature.SSO_SAML,
74
- EnterpriseFeature.RBAC,
75
- EnterpriseFeature.AUDIT_LOGGING,
76
- EnterpriseFeature.CUSTOM_BRANDING,
77
- EnterpriseFeature.API_RATE_LIMIT_INCREASE,
78
- EnterpriseFeature.PRIORITY_SUPPORT,
79
- EnterpriseFeature.SLA_GUARANTEE,
80
- EnterpriseFeature.MULTI_REGION,
81
- EnterpriseFeature.ADVANCED_REPORTS,
82
- }
83
- ),
84
- }
85
-
86
-
87
- @dataclass(frozen=True)
88
- class EnterpriseLicense:
89
- """Represents an enterprise license."""
90
-
91
- license_id: str
92
- tenant_id: str
93
- tier: LicenseTier
94
- issued_at: datetime
95
- expires_at: datetime
96
- features: frozenset[EnterpriseFeature] = field(default_factory=frozenset)
97
- max_users: int = 0
98
- max_projects: int = 0
99
- metadata: dict[str, Any] = field(default_factory=dict)
100
-
101
- def is_expired(self) -> bool:
102
- """Check if license is expired (without grace period)."""
103
- return datetime.now(UTC) > self.expires_at
104
-
105
- def is_in_grace_period(self) -> bool:
106
- """Check if license is in grace period."""
107
- now = datetime.now(UTC)
108
- if now <= self.expires_at:
109
- return False
110
- grace_end = self.expires_at + timedelta(days=GRACE_PERIOD_DAYS)
111
- return now <= grace_end
112
-
113
- def has_feature(self, feature: EnterpriseFeature) -> bool:
114
- """Check if license includes a specific feature."""
115
- tier_features = TIER_FEATURES.get(self.tier, frozenset())
116
- return feature in self.features or feature in tier_features
117
-
118
- def to_dict(self) -> dict[str, Any]:
119
- return {
120
- "license_id": self.license_id,
121
- "tenant_id": self.tenant_id,
122
- "tier": self.tier.value,
123
- "issued_at": self.issued_at.isoformat(),
124
- "expires_at": self.expires_at.isoformat(),
125
- "features": [f.value for f in self.features],
126
- "max_users": self.max_users,
127
- "max_projects": self.max_projects,
128
- "metadata": self.metadata,
129
- }
130
-
131
- @classmethod
132
- def from_dict(cls, data: dict[str, Any]) -> EnterpriseLicense:
133
- features = frozenset(EnterpriseFeature(f) for f in data.get("features", []))
134
- return cls(
135
- license_id=str(data["license_id"]),
136
- tenant_id=str(data["tenant_id"]),
137
- tier=LicenseTier(data["tier"]),
138
- issued_at=datetime.fromisoformat(str(data["issued_at"])),
139
- expires_at=datetime.fromisoformat(str(data["expires_at"])),
140
- features=features,
141
- max_users=int(data.get("max_users", 0)),
142
- max_projects=int(data.get("max_projects", 0)),
143
- metadata=data.get("metadata", {}),
144
- )
145
-
146
-
147
- @dataclass
148
- class LicenseCheckResult:
149
- """Result of a license check."""
150
-
151
- status: LicenseStatus
152
- license: EnterpriseLicense | None = None
153
- message: str | None = None
154
- days_until_expiry: int | None = None
155
- grace_days_remaining: int | None = None
156
-
157
-
158
- class LicenseValidator:
159
- """Validates enterprise licenses."""
160
-
161
- def __init__(self, signing_key: str) -> None:
162
- self._signing_key = signing_key
163
- self._cache: dict[str, tuple[float, LicenseCheckResult]] = {}
164
-
165
- def create_license_token(self, license: EnterpriseLicense) -> str:
166
- """Create a signed license token.
167
-
168
- The token format is: base64(payload).signature
169
- """
170
- payload = license.to_dict()
171
- payload_json = json.dumps(payload, separators=(",", ":"), sort_keys=True)
172
- payload_b64 = base64.urlsafe_b64encode(payload_json.encode()).decode().rstrip("=")
173
-
174
- signature = hmac.new(
175
- self._signing_key.encode(),
176
- payload_b64.encode(),
177
- hashlib.sha256,
178
- ).hexdigest()
179
-
180
- return f"{payload_b64}.{signature}"
181
-
182
- def validate_token(self, token: str) -> LicenseCheckResult:
183
- """Validate a license token and return the license if valid.
184
-
185
- Args:
186
- token: The signed license token
187
-
188
- Returns:
189
- LicenseCheckResult with status and license details
190
- """
191
- if not token:
192
- return LicenseCheckResult(
193
- status=LicenseStatus.MISSING,
194
- message="No license token provided",
195
- )
196
-
197
- parts = token.split(".")
198
- if len(parts) != 2:
199
- logger.warning("license.invalid_format")
200
- return LicenseCheckResult(
201
- status=LicenseStatus.INVALID,
202
- message="Invalid license token format",
203
- )
204
-
205
- payload_b64, signature = parts
206
-
207
- expected_sig = hmac.new(
208
- self._signing_key.encode(),
209
- payload_b64.encode(),
210
- hashlib.sha256,
211
- ).hexdigest()
212
-
213
- if not hmac.compare_digest(signature, expected_sig):
214
- logger.warning("license.invalid_signature")
215
- return LicenseCheckResult(
216
- status=LicenseStatus.INVALID,
217
- message="Invalid license signature",
218
- )
219
-
220
- try:
221
- padding = 4 - len(payload_b64) % 4
222
- if padding != 4:
223
- payload_b64 += "=" * padding
224
- payload_json = base64.urlsafe_b64decode(payload_b64).decode()
225
- payload = json.loads(payload_json)
226
- license = EnterpriseLicense.from_dict(payload)
227
- except (ValueError, json.JSONDecodeError, KeyError) as e:
228
- logger.warning("license.decode_error: %s", e)
229
- return LicenseCheckResult(
230
- status=LicenseStatus.INVALID,
231
- message="Failed to decode license",
232
- )
233
-
234
- now = datetime.now(UTC)
235
- days_until_expiry = (license.expires_at - now).days
236
-
237
- if license.is_expired():
238
- if license.is_in_grace_period():
239
- grace_end = license.expires_at + timedelta(days=GRACE_PERIOD_DAYS)
240
- grace_days = (grace_end - now).days
241
- logger.warning(
242
- "license.grace_period license_id=%s days_remaining=%d",
243
- license.license_id,
244
- grace_days,
245
- )
246
- return LicenseCheckResult(
247
- status=LicenseStatus.GRACE_PERIOD,
248
- license=license,
249
- message=f"License expired, {grace_days} days of grace period remaining",
250
- days_until_expiry=days_until_expiry,
251
- grace_days_remaining=grace_days,
252
- )
253
- else:
254
- logger.warning("license.expired license_id=%s", license.license_id)
255
- return LicenseCheckResult(
256
- status=LicenseStatus.EXPIRED,
257
- license=license,
258
- message="License has expired",
259
- days_until_expiry=days_until_expiry,
260
- )
261
-
262
- logger.debug(
263
- "license.valid license_id=%s tier=%s expires_in=%d",
264
- license.license_id,
265
- license.tier.value,
266
- days_until_expiry,
267
- )
268
- return LicenseCheckResult(
269
- status=LicenseStatus.VALID,
270
- license=license,
271
- days_until_expiry=days_until_expiry,
272
- )
273
-
274
- def check_cached(self, tenant_id: str, token: str) -> LicenseCheckResult:
275
- """Check license with caching to reduce validation overhead."""
276
- cache_key = f"{tenant_id}:{hashlib.sha256(token.encode()).hexdigest()[:16]}"
277
- now = time.time()
278
-
279
- if cache_key in self._cache:
280
- cached_time, cached_result = self._cache[cache_key]
281
- if now - cached_time < LICENSE_CHECK_INTERVAL:
282
- return cached_result
283
-
284
- result = self.validate_token(token)
285
- self._cache[cache_key] = (now, result)
286
- return result
287
-
288
- def clear_cache(self, tenant_id: str | None = None) -> None:
289
- """Clear license validation cache."""
290
- if tenant_id:
291
- self._cache = {
292
- k: v for k, v in self._cache.items() if not k.startswith(f"{tenant_id}:")
293
- }
294
- else:
295
- self._cache.clear()
296
-
297
-
298
- def require_feature(
299
- license_result: LicenseCheckResult,
300
- feature: EnterpriseFeature,
301
- ) -> tuple[bool, str | None]:
302
- """Check if a license allows access to a feature.
303
-
304
- Returns:
305
- Tuple of (allowed, error_message)
306
- """
307
- if license_result.status == LicenseStatus.MISSING:
308
- return False, "Enterprise license required"
309
-
310
- if license_result.status == LicenseStatus.INVALID:
311
- return False, "Invalid license"
312
-
313
- if license_result.status == LicenseStatus.EXPIRED:
314
- return False, "License has expired"
315
-
316
- if not license_result.license:
317
- return False, "No license data"
318
-
319
- if not license_result.license.has_feature(feature):
320
- return False, f"Feature {feature.value} not included in license"
321
-
322
- return True, None
323
-
324
-
325
- def require_enterprise(
326
- license_result: LicenseCheckResult,
327
- ) -> tuple[bool, str | None]:
328
- """Check if license is enterprise tier.
329
-
330
- Returns:
331
- Tuple of (allowed, error_message)
332
- """
333
- if license_result.status not in (LicenseStatus.VALID, LicenseStatus.GRACE_PERIOD):
334
- return False, "Enterprise license required"
335
-
336
- if not license_result.license:
337
- return False, "No license data"
338
-
339
- if license_result.license.tier != LicenseTier.ENTERPRISE:
340
- return False, "Enterprise tier license required"
341
-
342
- return True, None
portal/enterprise/rbac.py DELETED
@@ -1,276 +0,0 @@
1
- """Role-Based Access Control (RBAC) for enterprise portal.
2
-
3
- Security controls:
4
- - ASVS V8.2.1: Function-level authorization
5
- - Deterministic role mapping from SAML attributes
6
- - No default admin accounts
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import logging
12
- from dataclasses import dataclass, field
13
- from enum import Enum
14
- from typing import TYPE_CHECKING
15
-
16
- from kekkai_core import redact
17
-
18
- if TYPE_CHECKING:
19
- pass
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class Permission(Enum):
25
- """Available permissions in the system."""
26
-
27
- # Viewer permissions
28
- VIEW_FINDINGS = "view_findings"
29
- VIEW_DASHBOARD = "view_dashboard"
30
- VIEW_REPORTS = "view_reports"
31
-
32
- # Analyst permissions
33
- CREATE_UPLOAD = "create_upload"
34
- UPDATE_FINDING_STATUS = "update_finding_status"
35
- EXPORT_FINDINGS = "export_findings"
36
-
37
- # Admin permissions
38
- MANAGE_USERS = "manage_users"
39
- MANAGE_INTEGRATIONS = "manage_integrations"
40
- VIEW_AUDIT_LOGS = "view_audit_logs"
41
-
42
- # Tenant Admin permissions
43
- MANAGE_TENANT = "manage_tenant"
44
- MANAGE_SAML_CONFIG = "manage_saml_config"
45
- ROTATE_API_KEY = "rotate_api_key"
46
- DELETE_TENANT = "delete_tenant"
47
-
48
-
49
- class Role(Enum):
50
- """Available roles in the system."""
51
-
52
- VIEWER = "viewer"
53
- ANALYST = "analyst"
54
- ADMIN = "admin"
55
- TENANT_ADMIN = "tenant_admin"
56
-
57
-
58
- # Permission matrix: defines which roles have which permissions
59
- ROLE_PERMISSIONS: dict[Role, frozenset[Permission]] = {
60
- Role.VIEWER: frozenset(
61
- {
62
- Permission.VIEW_FINDINGS,
63
- Permission.VIEW_DASHBOARD,
64
- Permission.VIEW_REPORTS,
65
- }
66
- ),
67
- Role.ANALYST: frozenset(
68
- {
69
- Permission.VIEW_FINDINGS,
70
- Permission.VIEW_DASHBOARD,
71
- Permission.VIEW_REPORTS,
72
- Permission.CREATE_UPLOAD,
73
- Permission.UPDATE_FINDING_STATUS,
74
- Permission.EXPORT_FINDINGS,
75
- }
76
- ),
77
- Role.ADMIN: frozenset(
78
- {
79
- Permission.VIEW_FINDINGS,
80
- Permission.VIEW_DASHBOARD,
81
- Permission.VIEW_REPORTS,
82
- Permission.CREATE_UPLOAD,
83
- Permission.UPDATE_FINDING_STATUS,
84
- Permission.EXPORT_FINDINGS,
85
- Permission.MANAGE_USERS,
86
- Permission.MANAGE_INTEGRATIONS,
87
- Permission.VIEW_AUDIT_LOGS,
88
- }
89
- ),
90
- Role.TENANT_ADMIN: frozenset(
91
- {
92
- Permission.VIEW_FINDINGS,
93
- Permission.VIEW_DASHBOARD,
94
- Permission.VIEW_REPORTS,
95
- Permission.CREATE_UPLOAD,
96
- Permission.UPDATE_FINDING_STATUS,
97
- Permission.EXPORT_FINDINGS,
98
- Permission.MANAGE_USERS,
99
- Permission.MANAGE_INTEGRATIONS,
100
- Permission.VIEW_AUDIT_LOGS,
101
- Permission.MANAGE_TENANT,
102
- Permission.MANAGE_SAML_CONFIG,
103
- Permission.ROTATE_API_KEY,
104
- Permission.DELETE_TENANT,
105
- }
106
- ),
107
- }
108
-
109
- # SAML attribute to role mapping
110
- DEFAULT_ROLE_MAPPING: dict[str, Role] = {
111
- "viewer": Role.VIEWER,
112
- "analyst": Role.ANALYST,
113
- "admin": Role.ADMIN,
114
- "tenant_admin": Role.TENANT_ADMIN,
115
- "tenant-admin": Role.TENANT_ADMIN,
116
- "tenantadmin": Role.TENANT_ADMIN,
117
- }
118
-
119
-
120
- @dataclass(frozen=True)
121
- class AuthorizationResult:
122
- """Result of an authorization check."""
123
-
124
- allowed: bool
125
- role: Role | None = None
126
- permission: Permission | None = None
127
- reason: str | None = None
128
-
129
-
130
- @dataclass
131
- class UserContext:
132
- """Context for the current user session."""
133
-
134
- user_id: str
135
- tenant_id: str
136
- role: Role
137
- email: str | None = None
138
- display_name: str | None = None
139
- session_id: str | None = None
140
- permissions: frozenset[Permission] = field(default_factory=frozenset)
141
-
142
- def __post_init__(self) -> None:
143
- if not self.permissions:
144
- object.__setattr__(self, "permissions", ROLE_PERMISSIONS.get(self.role, frozenset()))
145
-
146
-
147
- class RBACManager:
148
- """Manages role-based access control decisions."""
149
-
150
- def __init__(
151
- self,
152
- role_mapping: dict[str, Role] | None = None,
153
- ) -> None:
154
- self._role_mapping = role_mapping or DEFAULT_ROLE_MAPPING.copy()
155
-
156
- def map_role_from_attribute(self, role_attribute: str) -> Role | None:
157
- """Map a SAML role attribute to a system role.
158
-
159
- Uses deterministic mapping - no user-configurable escalation.
160
- """
161
- normalized = role_attribute.lower().strip()
162
- return self._role_mapping.get(normalized)
163
-
164
- def map_role_from_attributes(self, attributes: dict[str, list[str]]) -> Role:
165
- """Map role from SAML attributes, defaulting to viewer."""
166
- role_attrs = attributes.get("role", []) + attributes.get("roles", [])
167
- role_attrs += attributes.get("group", []) + attributes.get("groups", [])
168
-
169
- for attr in role_attrs:
170
- mapped = self.map_role_from_attribute(attr)
171
- if mapped:
172
- return mapped
173
-
174
- return Role.VIEWER
175
-
176
- def get_permissions(self, role: Role) -> frozenset[Permission]:
177
- """Get all permissions for a role."""
178
- return ROLE_PERMISSIONS.get(role, frozenset())
179
-
180
- def has_permission(self, role: Role, permission: Permission) -> bool:
181
- """Check if a role has a specific permission."""
182
- return permission in ROLE_PERMISSIONS.get(role, frozenset())
183
-
184
- def authorize(
185
- self,
186
- user_context: UserContext,
187
- required_permission: Permission,
188
- resource_tenant_id: str | None = None,
189
- ) -> AuthorizationResult:
190
- """Authorize an action for a user.
191
-
192
- Args:
193
- user_context: Current user context with role
194
- required_permission: Permission needed for the action
195
- resource_tenant_id: Tenant owning the resource (for cross-tenant checks)
196
-
197
- Returns:
198
- AuthorizationResult indicating if access is allowed
199
- """
200
- if resource_tenant_id and resource_tenant_id != user_context.tenant_id:
201
- logger.warning(
202
- "authz.denied.cross_tenant user_id=%s tenant=%s target_tenant=%s permission=%s",
203
- redact(user_context.user_id),
204
- user_context.tenant_id,
205
- resource_tenant_id,
206
- required_permission.value,
207
- )
208
- return AuthorizationResult(
209
- allowed=False,
210
- role=user_context.role,
211
- permission=required_permission,
212
- reason="Cross-tenant access denied",
213
- )
214
-
215
- if not self.has_permission(user_context.role, required_permission):
216
- logger.warning(
217
- "authz.denied.permission user_id=%s tenant=%s role=%s permission=%s",
218
- redact(user_context.user_id),
219
- user_context.tenant_id,
220
- user_context.role.value,
221
- required_permission.value,
222
- )
223
- return AuthorizationResult(
224
- allowed=False,
225
- role=user_context.role,
226
- permission=required_permission,
227
- reason=f"Role {user_context.role.value} lacks {required_permission.value}",
228
- )
229
-
230
- logger.debug(
231
- "authz.allowed user_id=%s tenant=%s role=%s permission=%s",
232
- redact(user_context.user_id),
233
- user_context.tenant_id,
234
- user_context.role.value,
235
- required_permission.value,
236
- )
237
- return AuthorizationResult(
238
- allowed=True,
239
- role=user_context.role,
240
- permission=required_permission,
241
- )
242
-
243
- def create_user_context(
244
- self,
245
- user_id: str,
246
- tenant_id: str,
247
- role: Role,
248
- email: str | None = None,
249
- display_name: str | None = None,
250
- session_id: str | None = None,
251
- ) -> UserContext:
252
- """Create a user context with permissions derived from role."""
253
- return UserContext(
254
- user_id=user_id,
255
- tenant_id=tenant_id,
256
- role=role,
257
- email=email,
258
- display_name=display_name,
259
- session_id=session_id,
260
- permissions=self.get_permissions(role),
261
- )
262
-
263
-
264
- def require_permission(permission: Permission) -> AuthorizationResult:
265
- """Decorator helper to check permission before function execution.
266
-
267
- Usage in web handlers:
268
- result = require_permission(Permission.CREATE_UPLOAD)
269
- if not result.allowed:
270
- return unauthorized_response(result.reason)
271
- """
272
- return AuthorizationResult(
273
- allowed=False,
274
- permission=permission,
275
- reason=f"Permission {permission.value} required",
276
- )