Coverage for src / lexigram / admin / auth / integration.py: 50%
64 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1"""Auth integration for lexigram-admin.
3This module provides the ``AdminUser`` dataclass — the primary admin user
4representation that satisfies ``AuthenticatedUserProtocol`` — plus re-exports
5of relevant contracts types for consumer convenience.
7CROSS-EXT-02: Uses protocols from lexigram-contracts for dependency injection,
8eliminating direct cross-extension imports from lexigram-auth.
9"""
11from __future__ import annotations
13from dataclasses import dataclass, field
15from lexigram.contracts import AuthenticatedUserProtocol as AuthenticatedUserProtocol
16from lexigram.contracts.auth import PasswordHasherProtocol
18# ============================================================================
19# Admin User
20# ============================================================================
23@dataclass
24class AdminUser:
25 """Admin user implementing AuthenticatedUserProtocol protocol.
27 This class represents an authenticated admin user with
28 roles and permissions.
29 """
31 id: int | str
32 email: str
33 name: str
34 password_hash: str | None = None
35 is_active: bool = True
36 is_superuser: bool = False
38 roles: list[str] = field(default_factory=list)
39 permissions: set[str] = field(default_factory=set)
41 # Metadata
42 created_at: str | None = None
43 last_login: str | None = None
45 # Reference to framework auth user (set when delegated auth is active)
46 framework_user: AuthenticatedUserProtocol | None = None
48 @property
49 def user_id(self) -> str:
50 """Unique user identifier."""
51 if self.framework_user is not None:
52 return self.framework_user.user_id
53 return str(self.id)
55 @property
56 def username(self) -> str:
57 """Username of the authenticated user."""
58 if self.framework_user is not None:
59 return getattr(self.framework_user, "username", self.framework_user.name)
60 return self.name
62 @property
63 def is_verified(self) -> bool:
64 """Whether user is verified."""
65 if self.framework_user is not None:
66 return getattr(self.framework_user, "is_verified", self.is_active)
67 return self.is_active
69 @property
70 def is_authenticated(self) -> bool:
71 """Check if user is authenticated."""
72 if self.framework_user is not None:
73 return getattr(self.framework_user, "is_authenticated", self.is_active)
74 return self.is_active
76 @property
77 def is_admin(self) -> bool:
78 """Check if user is admin."""
79 if self.framework_user is not None:
80 return (
81 getattr(self.framework_user, "is_admin", False) or "admin" in self.roles
82 )
83 return self.is_superuser or "admin" in self.roles
85 def has_role(self, role: str) -> bool:
86 """Check if user has a specific role."""
87 if self.framework_user is not None and hasattr(self.framework_user, "has_role"):
88 return self.framework_user.has_role(role)
89 return role in self.roles or self.is_superuser
91 def has_permission(self, permission: str) -> bool:
92 """Check if user has a specific permission."""
93 if self.framework_user is not None and hasattr(
94 self.framework_user, "has_permission"
95 ):
96 return self.framework_user.has_permission(permission)
97 if self.is_superuser:
98 return True
99 if permission in self.permissions:
100 return True
101 parts = permission.split(":")
102 for i in range(len(parts)):
103 wildcard = ":".join(parts[: i + 1]) + ":*"
104 if wildcard in self.permissions:
105 return True
106 return False
108 def has_any_permission(self, permissions: list[str]) -> bool:
109 """Check if user has any of the given permissions."""
110 return any(self.has_permission(p) for p in permissions)
112 def has_all_permissions(self, permissions: list[str]) -> bool:
113 """Check if user has all of the given permissions."""
114 return all(self.has_permission(p) for p in permissions)
117__all__ = [
118 "AdminUser",
119 "AuthenticatedUserProtocol",
120 "PasswordHasherProtocol",
121]