@dataclass
class Identity:
"""Represents the authenticated identity of a user within the application.
This class is typically populated from an external identity provider (e.g.
LDAP, OIDC, SAML) and used throughout the system for authorization,
auditing, and personalization.
Attributes
----------
subject
Globally unique identifier for the user (e.g. LDAP uid, OIDC sub).
username
Short, human-friendly login name, if available.
email
Primary email address of the user, if available.
display_name
Human-readable name to show in the UI, if available.
groups
Collection of group names or roles the user is a member of.
permissions
Specific permissions or scopes granted to the user.
claims
Arbitrary key/value pairs about the user, as provided by the identity
provider. This can include standard token claims (e.g. issuer, scopes,
tenant, custom attributes) or raw LDAP attributes. It is a flexible
extension point for carrying additional identity metadata without
changing the core Identity fields.
issued_at
Timestamp when this identity was issued / loaded.
expires_at
Optional timestamp when this identity should be considered invalid
(e.g. token expiry, session timeout).
audience
Optional list of logical audiences for which this identity is valid. In
token-based authentication this usually maps to the `aud` claim
(e.g. OAuth2/OIDC client IDs or services). It allows the application to
verify that an identity is meant to be used by this specific
application or API.
role
Optional high-level role assigned to the user (e.g. "user", "admin",
"manager"). This is distinct from groups and permissions, and is often
used for coarse-grained access control or UI personalization.
admin
Indicates whether the user has elevated administrative privileges.
pretend_identity
When set, indicates that this identity is impersonating another subject
(e.g. admin acting on behalf of a user).
"""
subject: str # unique user id (sub / dn / uid)
username: str | None
email: str | None
display_name: str | None
groups: Sequence[str | Group]
permissions: Sequence[str]
claims: Mapping[str, Any]
issued_at: datetime
expires_at: datetime | None = None
role: str | None = None
audience: Sequence[str] | None = None
admin: bool = False
pretend_identity: Self | None = None
@property
def has_admin_privileges(self) -> bool:
"""Check if the identity has administrative privileges.
Returns
-------
bool
True if the identity is an admin, False otherwise.
"""
if self.admin is True:
return True
if self.role and self.role.lower() == "admin":
return True
if "admin" in (p.lower() for p in self.permissions):
return True
return False
@classmethod
def from_ldap_dict(
cls,
entry: Mapping[str, Any],
mappings: Mapping[str, str] = DEFAULT_LDAP_MAPPINGS,
populate_groups: bool = True,
populate_permissions: bool = False,
populate_claims: bool = True,
) -> "Identity":
"""Instantiate an Identity from an LDAP entry dictionary.
Parameters
----------
entry
LDAP entry dictionary containing user attributes.
mappings
Attribute mappings from LDAP fields to Identity fields, by default
DEFAULT_LDAP_MAPPINGS
populate_groups
Whether to extract groups from the LDAP entry, by default True
populate_permissions
Whether to extract permissions from the LDAP entry, by default
False
populate_claims
Whether to populate the claims dictionary from the LDAP entry, by
default True
Returns
-------
Identity
An Identity instance populated with data from the LDAP entry.
"""
username = cls._get_key(entry, mappings["username"])
if not username:
username = cls._get_key(entry, mappings["subject"])
return cls(
subject=cls._get_key(entry, mappings["subject"], ""),
username=username,
email=cls._get_key(entry, mappings["email"]),
display_name=cls._get_key(entry, mappings["display_name"]),
groups=cls._extract_groups(entry) if populate_groups else [],
permissions=(
cls._get_key(
entry,
mappings["permissions"],
default=[],
return_type=list,
)
if populate_permissions
else []
),
claims=(
cls._remove_password_from_claims(entry)
if populate_claims
else {}
),
issued_at=datetime.now(tz=timezone.utc),
)
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> "Identity":
"""Instantiate an Identity from a generic dictionary.
Parameters
----------
data
Dictionary containing identity attributes.
Returns
-------
Identity
An Identity instance populated with data from the dictionary.
"""
issued_at = data.get("issued_at", datetime.now(tz=timezone.utc))
if isinstance(issued_at, str):
issued_at = datetime.fromisoformat(issued_at)
expires_at = data.get("expires_at", None)
if isinstance(expires_at, str):
expires_at = datetime.fromisoformat(expires_at)
return cls(
subject=data.get("subject", ""),
username=data.get("username", None),
email=data.get("email", None),
display_name=data.get("display_name", None),
groups=data.get("groups", []),
permissions=data.get("permissions", []),
claims=data.get("claims", {}),
issued_at=issued_at,
expires_at=expires_at,
audience=data.get("audience", None),
role=data.get("role", None),
admin=data.get("admin", False),
pretend_identity=(
Identity.from_dict(data.get("pretend_identity", {}))
if data.get("pretend_identity")
else None
),
)
@classmethod
def from_user(cls, user: User) -> "Identity":
"""Instantiate an Identity from a User instance.
Parameters
----------
user
User object to create the Identity from.
Returns
-------
Identity
An Identity instance populated with data from the User object.
"""
subject = str(user.id) if user.id else user.username
return cls(
subject=subject, # type: ignore
username=user.username,
email=user.email,
display_name=user.display_name,
groups=user.groups or [],
permissions=user.permissions or [],
claims={},
issued_at=datetime.now(tz=timezone.utc),
role=user.role, # type: ignore
admin=user.admin,
)
def __str__(self) -> str:
return self.subject
def __repr__(self) -> str:
return (
"Identity("
f"subject={self.subject!r}, "
f"username={self.username!r}, "
f"email={self.email!r}, "
f"display_name={self.display_name!r}, "
f"groups={self.groups!r}, "
f"permissions={self.permissions!r}, "
f"claims={self.claims!r}, "
f"issued_at={self.issued_at!r}, "
f"expires_at={self.expires_at!r}, "
f"audience={self.audience!r}, "
f"role={self.role!r}, "
f"admin={self.admin!r}, "
f"pretend_identity={self.pretend_identity!r}"
")"
)
def update_from_user(self, user: User) -> None:
"""Update the Identity instance with data from a User instance.
Parameters
----------
user
User object to update from.
"""
self.username = user.username
if not self.email:
self.email = user.email
if not self.display_name:
self.display_name = user.display_name
for permission in user.permissions or []:
self.permissions = self._append_on_sequence(
self.permissions, permission
)
for group in user.groups or []:
self.groups = self._append_on_sequence(self.groups, group)
self.role = user.role # type: ignore
self.admin = user.admin
def update_from_groups(self, groups: list[Group]) -> None:
"""Update the Identity permissions with data from a list of Group
instances.
Parameters
----------
groups
List of Group objects to update from.
"""
for group in groups:
for permission in group.permissions or []:
self.permissions = self._append_on_sequence(
self.permissions, permission
)
def to_dict(self) -> dict[str, Any]:
"""Convert the Identity instance to a dictionary.
Returns
-------
dict[str, Any]
A dictionary representation of the Identity instance.
"""
return {
"subject": self.subject,
"username": self.username,
"email": self.email,
"display_name": self.display_name,
"groups": self.groups,
"permissions": self.permissions,
"claims": self.claims,
"issued_at": self.issued_at.isoformat(),
"expires_at": (
self.expires_at.isoformat() if self.expires_at else None
),
"audience": self.audience,
"role": self.role,
"admin": self.admin,
"pretend_identity": (
self.pretend_identity.to_dict()
if self.pretend_identity
else None
),
}
@staticmethod
def _get_key(
obj: Mapping[str, Any],
key: str,
default: Any = None,
return_type: type = str,
) -> Any:
"""Helper method to get a key from a dictionary with type casting.
Parameters
----------
obj
Source dictionary.
key
Key to retrieve.
default
Default value to return if the key is not found, by default None
return_type
Expected return type for the value, by default str
Returns
-------
Any
The value associated with the key, cast to the specified type. If
the key is not found, returns the default value.
"""
value: str | list[Any] | None = obj.get(key, default)
if value is not None and not isinstance(value, return_type):
if isinstance(value, list) and len(value) > 0:
value = value[0]
try:
value = return_type(value)
except (ValueError, TypeError):
value = default
return value
@staticmethod
def _remove_password_from_claims(
claims: Mapping[str, Any],
) -> Mapping[str, Any]:
"""Return a copy of the Identity with password-related claims removed.
This method creates a new Identity instance with sensitive password
information removed from the claims dictionary to enhance security.
Parameters
----------
claims
Original claims dictionary.
Returns
-------
Mapping[str, Any]
A new Identity instance without password-related claims.
"""
filtered_claims = {
k: v for k, v in claims.items() if "password" not in k.lower()
}
return filtered_claims
@staticmethod
def _extract_groups(
entry: Mapping[str, Any],
) -> list[str]:
"""Extract group names from the LDAP entry's memberOf attribute.
Parameters
----------
entry
LDAP entry dictionary containing user attributes.
Returns
-------
list[str]
A list of group names the user is a member of.
"""
groups: list[str] = []
for item in entry.get("memberOf", []):
group = (
item.replace("\\,", ";")
.split(",")[0]
.replace(";", ",")
.replace("CN=", "")
.replace("cn=", "")
)
groups.append(group)
return groups
def _append_on_sequence(
self, sequence: Sequence[Any], item: Any
) -> Sequence[Any]:
"""Helper method to append an item to a sequence if it's not already
present.
Parameters
----------
sequence
The original sequence to append to.
item
The item to append if not already in the sequence.
Returns
-------
Sequence[str]
A new sequence with the item appended if it was not already
present.
"""
if item not in sequence:
return list(sequence) + [item]
return sequence