|
14 | 14 | import logging |
15 | 15 | import os |
16 | 16 | import re |
| 17 | +from datetime import datetime |
17 | 18 | from enum import Enum, StrEnum |
18 | 19 | from pathlib import Path |
19 | 20 | from typing import Any, Generic, List, Optional, Self, TypeVar, Union |
@@ -1487,6 +1488,56 @@ def plugin_name(self, name: str) -> None: |
1487 | 1488 | self._plugin_name = name |
1488 | 1489 |
|
1489 | 1490 |
|
| 1491 | +class UserContext(BaseModel): |
| 1492 | + """Authenticated user identity context for propagation to upstream servers and plugins. |
| 1493 | +
|
| 1494 | + Attributes: |
| 1495 | + user_id: Primary user identifier (typically email). |
| 1496 | + email: User email address. |
| 1497 | + full_name: User's display name. |
| 1498 | + is_admin: Whether the user has admin privileges. |
| 1499 | + groups: User's group memberships. |
| 1500 | + roles: User's RBAC roles. |
| 1501 | + team_id: Current team context (for single-team API tokens). |
| 1502 | + teams: All teams the user belongs to. |
| 1503 | + department: User's department. |
| 1504 | + attributes: Additional user attributes (extensible). |
| 1505 | + auth_method: How the user authenticated (bearer, api_key, basic, sso, proxy). |
| 1506 | + authenticated_at: When the authentication occurred. |
| 1507 | + service_account: Set when a service account is acting on behalf of this user. |
| 1508 | + delegation_chain: Chain of delegated identities for audit trail. |
| 1509 | +
|
| 1510 | + Examples: |
| 1511 | + >>> uc = UserContext(user_id="alice@example.com") |
| 1512 | + >>> uc.user_id |
| 1513 | + 'alice@example.com' |
| 1514 | + >>> uc.is_admin |
| 1515 | + False |
| 1516 | + >>> uc.groups |
| 1517 | + [] |
| 1518 | + >>> uc2 = UserContext(user_id="bob@example.com", email="bob@example.com", is_admin=True, auth_method="bearer") |
| 1519 | + >>> uc2.is_admin |
| 1520 | + True |
| 1521 | + >>> uc2.auth_method |
| 1522 | + 'bearer' |
| 1523 | + """ |
| 1524 | + |
| 1525 | + user_id: str |
| 1526 | + email: Optional[str] = None |
| 1527 | + full_name: Optional[str] = None |
| 1528 | + is_admin: bool = False |
| 1529 | + groups: list[str] = Field(default_factory=list) |
| 1530 | + roles: list[str] = Field(default_factory=list) |
| 1531 | + team_id: Optional[str] = None |
| 1532 | + teams: Optional[list[str]] = None |
| 1533 | + department: Optional[str] = None |
| 1534 | + attributes: dict[str, Any] = Field(default_factory=dict) |
| 1535 | + auth_method: Optional[str] = None |
| 1536 | + authenticated_at: Optional[datetime] = None |
| 1537 | + service_account: Optional[str] = None |
| 1538 | + delegation_chain: list[str] = Field(default_factory=list) |
| 1539 | + |
| 1540 | + |
1490 | 1541 | class Config(BaseModel): |
1491 | 1542 | """Configurations for plugins. |
1492 | 1543 |
|
@@ -1582,6 +1633,7 @@ class GlobalContext(BaseModel): |
1582 | 1633 | Attributes: |
1583 | 1634 | request_id (str): ID of the HTTP request. |
1584 | 1635 | user (str): user ID associated with the request. |
| 1636 | + user_context (Optional[UserContext]): structured user identity context. |
1585 | 1637 | tenant_id (str): tenant ID. |
1586 | 1638 | server_id (str): server ID. |
1587 | 1639 | content_type (Optional[str]): Content-Type header from the request. |
@@ -1611,6 +1663,7 @@ class GlobalContext(BaseModel): |
1611 | 1663 |
|
1612 | 1664 | request_id: str |
1613 | 1665 | user: Optional[Union[str, dict[str, Any]]] = None |
| 1666 | + user_context: Optional[UserContext] = None |
1614 | 1667 | tenant_id: Optional[str] = None |
1615 | 1668 | server_id: Optional[str] = None |
1616 | 1669 | content_type: Optional[str] = None |
@@ -1664,6 +1717,63 @@ class PluginContext(BaseModel): |
1664 | 1717 | global_context: GlobalContext |
1665 | 1718 | metadata: dict[str, Any] = Field(default_factory=dict) |
1666 | 1719 |
|
| 1720 | + @property |
| 1721 | + def user_context(self) -> Optional[UserContext]: |
| 1722 | + """Get the authenticated user context. |
| 1723 | +
|
| 1724 | + Returns: |
| 1725 | + The UserContext if available, None otherwise. |
| 1726 | +
|
| 1727 | + Examples: |
| 1728 | + >>> gctx = GlobalContext(request_id="req-1") |
| 1729 | + >>> ctx = PluginContext(global_context=gctx) |
| 1730 | + >>> ctx.user_context is None |
| 1731 | + True |
| 1732 | + """ |
| 1733 | + return self.global_context.user_context |
| 1734 | + |
| 1735 | + @property |
| 1736 | + def user_email(self) -> Optional[str]: |
| 1737 | + """Get the authenticated user's email. |
| 1738 | +
|
| 1739 | + Falls back to the legacy ``global_context.user`` field when no |
| 1740 | + structured UserContext is available. |
| 1741 | +
|
| 1742 | + Returns: |
| 1743 | + User email string or None. |
| 1744 | +
|
| 1745 | + Examples: |
| 1746 | + >>> gctx = GlobalContext(request_id="req-1", user="alice@example.com") |
| 1747 | + >>> ctx = PluginContext(global_context=gctx) |
| 1748 | + >>> ctx.user_email |
| 1749 | + 'alice@example.com' |
| 1750 | + """ |
| 1751 | + uc = self.global_context.user_context |
| 1752 | + if uc: |
| 1753 | + return uc.email |
| 1754 | + user = self.global_context.user |
| 1755 | + if isinstance(user, str): |
| 1756 | + return user |
| 1757 | + if isinstance(user, dict): |
| 1758 | + return user.get("email") |
| 1759 | + return None |
| 1760 | + |
| 1761 | + @property |
| 1762 | + def user_groups(self) -> list[str]: |
| 1763 | + """Get the authenticated user's groups. |
| 1764 | +
|
| 1765 | + Returns: |
| 1766 | + List of group names. Empty if no user context. |
| 1767 | +
|
| 1768 | + Examples: |
| 1769 | + >>> gctx = GlobalContext(request_id="req-1") |
| 1770 | + >>> ctx = PluginContext(global_context=gctx) |
| 1771 | + >>> ctx.user_groups |
| 1772 | + [] |
| 1773 | + """ |
| 1774 | + uc = self.global_context.user_context |
| 1775 | + return uc.groups if uc else [] |
| 1776 | + |
1667 | 1777 | def get_state(self, key: str, default: Any = None) -> Any: |
1668 | 1778 | """Get value from shared state. |
1669 | 1779 |
|
@@ -1732,8 +1842,8 @@ class PluginPackageInfo(BaseModel): |
1732 | 1842 |
|
1733 | 1843 | Examples: |
1734 | 1844 | >>> pkg = PluginPackageInfo(git_repository="https://github.com/user/repo.git", |
1735 | | - git_branch_tag_commit="v1.0.0", |
1736 | | - version_constraint=">=1.0.0") |
| 1845 | + ... git_branch_tag_commit="v1.0.0", |
| 1846 | + ... version_constraint=">=1.0.0") |
1737 | 1847 | >>> pkg2 = PluginPackageInfo(pypi_package="my-package", version_constraint=">=1.0.0") |
1738 | 1848 | """ |
1739 | 1849 |
|
|
0 commit comments