Skip to content

Security Module

The security module provides utilities for implementing security features in Spryx applications, including permission handling, token claims validation, and related functionality.

Key Features

  • Permission Management: Typed permission enums with consistent format
  • Token Claims: Pydantic models for validating JWT claims
  • Type Safety: Strong typing for security-related components

API Reference

Permissions

Permission definitions for Spryx applications.

This module defines standardized permission strings in a consistent format (resource:action) that can be used for role-based access control.

Classes

Permission

Bases: StrEnum

Standard permission strings for Spryx applications.

Permissions follow the format resource:action where: - resource: The entity being accessed (users, orders, etc.) - action: The operation being performed (read, write, etc.)

Source code in spryx_core/security/permissions.py
@unique
class Permission(StrEnum):
    """Standard permission strings for Spryx applications.

    Permissions follow the format `resource:action` where:
    - resource: The entity being accessed (users, orders, etc.)
    - action: The operation being performed (read, write, etc.)
    """

    READ_MEMBER = "member:read"
    LIST_MEMBERS = "member:list"
    INVITE_MEMBER = "member:invite"
    CANCEL_INVITE = "member:cancel_invite"
    UPDATE_MEMBER_ROLE = "member:update_role"
    REMOVE_MEMBER = "member:remove"

    CREATE_AGENT = "agent:create"
    READ_AGENT = "agent:read"
    LIST_AGENTS = "agent:list"
    UPDATE_AGENT = "agent:update"
    DELETE_AGENT = "agent:delete"

    CREATE_CREDENTIAL = "credential:create"
    READ_CREDENTIAL = "credential:read"
    LIST_CREDENTIALS = "credential:list"
    UPDATE_CREDENTIAL = "credential:update"
    DELETE_CREDENTIAL = "credential:delete"

    UPLOAD_FILE = "file:upload"
    READ_FILE = "file:read"
    LIST_FILES = "file:list"
    UPDATE_FILE = "file:update"
    DELETE_FILE = "file:delete"

    CREATE_VECTOR_STORE = "vector_store:create"
    READ_VECTOR_STORE = "vector_store:read"
    LIST_VECTOR_STORES = "vector_store:list"
    UPDATE_VECTOR_STORE = "vector_store:update"
    DELETE_VECTOR_STORE = "vector_store:delete"

    CREATE_CHANNEL = "channel:create"
    READ_CHANNEL = "channel:read"
    LIST_CHANNELS = "channel:list"
    UPDATE_CHANNEL = "channel:update"
    DELETE_CHANNEL = "channel:delete"
    CONNECT_CHANNEL = "channel:connect"
    DISCONNECT_CHANNEL = "channel:disconnect"

    CREATE_CONTACT = "contact:create"
    READ_CONTACT = "contact:read"
    LIST_CONTACTS = "contact:list"
    UPDATE_CONTACT = "contact:update"
    DELETE_CONTACT = "contact:delete"

    READ_MESSAGES = "messages:read"
    SEND_MESSAGES = "messages:send"
    DELETE_MESSAGES = "messages:delete"

    @classmethod
    def has_permission(
        cls,
        user_permissions: List[Permission] | Set[Permission],
        required_permission: Permission,
    ) -> bool:
        """Check if the given permissions include the required permission.

        Args:
            user_permissions: List or set of permissions to check
            required_permission: The permission to look for

        Returns:
            True if the required permission is in the user_permissions
        """
        return required_permission in user_permissions

    @classmethod
    def has_all_permissions(
        cls,
        user_permissions: List[Permission] | Set[Permission],
        required_permissions: List[Permission] | Set[Permission],
    ) -> bool:
        """Check if the given permissions include all the required permissions.

        Args:
            user_permissions: List or set of permissions to check
            required_permissions: List or set of permissions to look for

        Returns:
            True if all required permissions are in the user_permissions
        """
        if isinstance(required_permissions, list):
            required_permissions_set = set(required_permissions)
        else:
            required_permissions_set = required_permissions

        if isinstance(user_permissions, list):
            user_permissions_set = set(user_permissions)
        else:
            user_permissions_set = user_permissions

        return required_permissions_set.issubset(user_permissions_set)
Functions
has_all_permissions(user_permissions, required_permissions) classmethod

Check if the given permissions include all the required permissions.

Parameters:

Name Type Description Default
user_permissions List[Permission] | Set[Permission]

List or set of permissions to check

required
required_permissions List[Permission] | Set[Permission]

List or set of permissions to look for

required

Returns:

Type Description
bool

True if all required permissions are in the user_permissions

Source code in spryx_core/security/permissions.py
@classmethod
def has_all_permissions(
    cls,
    user_permissions: List[Permission] | Set[Permission],
    required_permissions: List[Permission] | Set[Permission],
) -> bool:
    """Check if the given permissions include all the required permissions.

    Args:
        user_permissions: List or set of permissions to check
        required_permissions: List or set of permissions to look for

    Returns:
        True if all required permissions are in the user_permissions
    """
    if isinstance(required_permissions, list):
        required_permissions_set = set(required_permissions)
    else:
        required_permissions_set = required_permissions

    if isinstance(user_permissions, list):
        user_permissions_set = set(user_permissions)
    else:
        user_permissions_set = user_permissions

    return required_permissions_set.issubset(user_permissions_set)
has_permission(user_permissions, required_permission) classmethod

Check if the given permissions include the required permission.

Parameters:

Name Type Description Default
user_permissions List[Permission] | Set[Permission]

List or set of permissions to check

required
required_permission Permission

The permission to look for

required

Returns:

Type Description
bool

True if the required permission is in the user_permissions

Source code in spryx_core/security/permissions.py
@classmethod
def has_permission(
    cls,
    user_permissions: List[Permission] | Set[Permission],
    required_permission: Permission,
) -> bool:
    """Check if the given permissions include the required permission.

    Args:
        user_permissions: List or set of permissions to check
        required_permission: The permission to look for

    Returns:
        True if the required permission is in the user_permissions
    """
    return required_permission in user_permissions

PlatformPermission

Bases: StrEnum

Standard permission strings for Spryx platform.

Permissions follow the format resource:action where: - resource: The entity being accessed (organization, application, etc.) - action: The operation being performed (read, write, etc.)

Source code in spryx_core/security/permissions.py
@unique
class PlatformPermission(StrEnum):
    """Standard permission strings for Spryx platform.

    Permissions follow the format `resource:action` where:
    - resource: The entity being accessed (organization, application, etc.)
    - action: The operation being performed (read, write, etc.)
    """

    ADMIN_ORGANIZATIONS = "organizations:admin"

    CREATE_ORGANIZATION = "organization:create"
    LIST_ORGANIZATIONS = "organization:list"
    READ_ORGANIZATION = "organization:read"
    UPDATE_ROLE_USER_ORGANIZATION = "organization:update_role_user"
    UPDATE_ORGANIZATION = "organization:update"
    DELETE_ORGANIZATION = "organization:delete"

    UPDATE_USER_PLATFORM_ROLE = "user:update_platform_role"

    # Application permissions
    CREATE_APPLICATION = "application:create"
    READ_APPLICATION = "application:read"
    LIST_APPLICATIONS = "application:list"
    UPDATE_APPLICATION = "application:update"
    DELETE_APPLICATION = "application:delete"

    # Plan permissions
    CREATE_PLAN = "plan:create"
    READ_PLAN = "plan:read"
    LIST_PLANS = "plan:list"
    UPDATE_PLAN = "plan:update"
    DELETE_PLAN = "plan:delete"

    # User permissions
    CREATE_USER = "user:create"
    READ_USER = "user:read"
    LIST_USERS = "user:list"
    UPDATE_USER = "user:update"
    DELETE_USER = "user:delete"

Claims

JWT claims validation models for Spryx authentication.

This module provides Pydantic models for validating and working with JWT claims used in Spryx authentication system. It includes models for different token types (user and application tokens) with automatic discrimination between them.

Classes

AppClaims

Bases: BaseClaims

Token issued to a machine / application integrating with Spryx.

Source code in spryx_core/security/claims.py
class AppClaims(BaseClaims):
    """Token issued to a *machine* / application integrating with Spryx."""

    token_type: Literal["app"] = Field(
        ..., description="Must be 'app' for application tokens"
    )

BaseClaims

Bases: _CoreModel

Fields common to every access token issued by Spryx Auth.

Source code in spryx_core/security/claims.py
class BaseClaims(_CoreModel):
    """Fields common to every access token issued by Spryx Auth."""

    iss: str = Field(..., description="Issuer of the token")
    sub: str = Field(..., description="Subject of the token (user or app ID)")
    aud: str = Field(..., description="Audience for the token (client ID)")
    iat: datetime = Field(..., description="Issued at timestamp")
    jti: str = Field(..., description="JWT ID")
    exp: datetime = Field(..., description="Expiration timestamp")
    token_type: str = Field(..., frozen=True, description="Type of token (user or app)")

    @model_validator(mode="after")
    def _check_exp(self):
        """Validate that the token hasn't expired."""
        if self.exp < datetime.now(UTC):
            raise ValueError("token already expired")
        return self

UserClaims

Bases: BaseClaims

Token issued to a human user belonging to an organization.

Source code in spryx_core/security/claims.py
class UserClaims(BaseClaims):
    """Token issued to a *human* user belonging to an organization."""

    token_type: Literal["user"] = Field(
        ..., description="Must be 'user' for user tokens"
    )
    name: str = Field(..., description="Name of the user")
    email: str = Field(..., description="Email of the user")
    image: Optional[str] = Field(None, description="Image of the user")
    current_organization: Optional[CurrentOrganization] = Field(
        None, description="Current organization of the user"
    )
    allowed_org_ids: list[EntityId] = Field(
        default_factory=list, description="IDs of organizations the user has access to"
    )
    platform_role: str = Field(..., description="Role of the user")
    platform_permissions: list[PlatformPermission] = Field(
        default_factory=list, description="Permissions of the user"
    )

Functions

is_app_claims(claims)

Check if the token claims are for an application token.

Source code in spryx_core/security/claims.py
def is_app_claims(claims: TokenClaims) -> bool:
    """Check if the token claims are for an application token."""
    return claims.token_type == "app"

is_user_claims(claims)

Check if the token claims are for a user token.

Source code in spryx_core/security/claims.py
def is_user_claims(claims: TokenClaims) -> bool:
    """Check if the token claims are for a user token."""

Usage Examples

Working with Permissions

The Permission enum defines standardized permission strings:

from spryx_core import Permission

# Check if user has specific permissions
def check_user_permissions(user_permissions, required_permissions):
    return all(perm in user_permissions for perm in required_permissions)

# Using with typed permissions
user_permissions = [Permission.READ_USERS, Permission.READ_ORDERS]
can_manage_users = check_user_permissions(user_permissions, [Permission.READ_USERS, Permission.WRITE_USERS])

Validating Token Claims

The claims models provide type-safe validation for JWT tokens:

from spryx_core import TokenClaims
import jwt
from datetime import datetime, timedelta, UTC

# Example JWT payload
payload = {
    "iss": "spryx-auth",
    "sub": "user123",
    "aud": "my-app",
    "iat": datetime.now(UTC),
    "exp": datetime.now(UTC) + timedelta(hours=1),
    "token_type": "user",
    "permissions": ["users:read", "orders:read"]
}

# Encode token (in a real app, this would be done by the auth server)
secret = "your-secret-key"
token = jwt.encode(payload, secret, algorithm="HS256")

# Decode and validate token
def validate_token(token):
    try:
        # Decode token
        decoded = jwt.decode(token, secret, algorithms=["HS256"])

        # Validate and cast to appropriate type
        claims = TokenClaims.model_validate(decoded)

        # The claims object will be either UserClaims or AppClaims
        # based on the token_type discriminator
        return claims
    except Exception as e:
        print(f"Token validation failed: {e}")
        return None

# Use the validated claims
claims = validate_token(token)
if claims and hasattr(claims, "permissions"):
    # It's a UserClaims object
    user_permissions = claims.permissions
    print(f"User has permissions: {user_permissions}")

Difference Between UserClaims and AppClaims

Spryx supports two types of tokens:

  1. User Tokens (UserClaims): Issued to human users and contain user-specific permissions.
  2. App Tokens (AppClaims): Issued to machine clients/applications and contain scopes rather than permissions.

The TokenClaims type uses discriminated unions to automatically cast to the correct type based on the token_type field.