Skip to content

API Reference

User API key management module.

This module provides API key lifecycle management including creation, validation, revocation, and deletion.

Exports
  • CRUD: get_api_keys_by_user_id, get_api_key_by_id, get_api_key_by_hash, create_api_key, update_last_used, revoke_api_key, delete_api_key
  • Schemas: UsersApiKeyCreate, UsersApiKeyRead, UsersApiKeyCreated
  • Models: UsersApiKeys (ORM model)
  • Utils: generate_api_key, hash_api_key, validate_api_key_scopes

UsersApiKeyCreate

Bases: BaseModel

Schema for creating a new API key.

API-key creation is a sensitive, persistent grant of account access — it must be gated by step-up verification. The caller MUST supply current_password, and an MFA code when MFA is enabled on the account.

For SSO-only accounts (no local password set), the password field may be omitted and the password check is skipped — see :func:users.users.utils.verify_step_up_credentials for the rationale and the known coverage gap.

Attributes:

Name Type Description
name StrictStr

User-friendly label for the key.

scopes list[StrictStr]

List containing the supported API key scope.

expires_at datetime | None

Optional expiration datetime. None means the key never expires.

current_password StrictStr | None

Caller's existing password (step-up verification). Required when the account has a local password; may be omitted for SSO-only accounts.

mfa_code StrictStr | None

TOTP or backup code, required when MFA is enabled on the account.

Source code in backend/app/users/users_api_keys/schema.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class UsersApiKeyCreate(BaseModel):
    """
    Schema for creating a new API key.

    API-key creation is a sensitive, persistent grant of
    account access — it must be gated by step-up
    verification. The caller MUST supply ``current_password``,
    and an MFA code when MFA is enabled on the account.

    For SSO-only accounts (no local password set), the password
    field may be omitted and the password check is skipped — see
    :func:`users.users.utils.verify_step_up_credentials` for the
    rationale and the known coverage gap.

    Attributes:
        name: User-friendly label for the key.
        scopes: List containing the supported API key scope.
        expires_at: Optional expiration datetime.
            None means the key never expires.
        current_password: Caller's existing password
            (step-up verification). Required when the account
            has a local password; may be omitted for SSO-only
            accounts.
        mfa_code: TOTP or backup code, required when MFA
            is enabled on the account.
    """

    name: StrictStr = Field(
        ...,
        min_length=1,
        max_length=100,
        description="User-friendly label for the key",
    )
    scopes: list[StrictStr] = Field(
        ...,
        min_length=1,
        description="List of scope strings to grant",
    )
    expires_at: datetime | None = Field(
        None,
        description=(
            "Optional expiration datetime. " "None means the key never expires."
        ),
    )
    current_password: StrictStr | None = Field(
        default=None,
        min_length=1,
        max_length=250,
        description=(
            "Current password (step-up verification). Required"
            " when the account has a local password."
        ),
    )
    mfa_code: StrictStr | None = Field(
        default=None,
        max_length=32,
        description="TOTP or backup code, required when MFA is enabled",
    )

    @field_validator("scopes")
    @classmethod
    def scopes_must_be_valid(cls, v: list[str]) -> list[str]:
        """
        Validate API keys only grant activity upload access.

        Args:
            v: List of scope strings to validate.

        Returns:
            Validated list of scope strings.

        Raises:
            ValueError: If any scope is not supported.
        """
        allowed = {"activities:upload"}
        unsupported = set(v) - allowed
        if unsupported:
            raise ValueError(
                "Unsupported API key scopes: "
                f"{unsupported}. Valid scopes: {allowed}"
            )
        return v

    model_config = ConfigDict(from_attributes=True)

scopes_must_be_valid classmethod

scopes_must_be_valid(v)

Validate API keys only grant activity upload access.

Parameters:

Name Type Description Default
v list[str]

List of scope strings to validate.

required

Returns:

Type Description
list[str]

Validated list of scope strings.

Raises:

Type Description
ValueError

If any scope is not supported.

Source code in backend/app/users/users_api_keys/schema.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@field_validator("scopes")
@classmethod
def scopes_must_be_valid(cls, v: list[str]) -> list[str]:
    """
    Validate API keys only grant activity upload access.

    Args:
        v: List of scope strings to validate.

    Returns:
        Validated list of scope strings.

    Raises:
        ValueError: If any scope is not supported.
    """
    allowed = {"activities:upload"}
    unsupported = set(v) - allowed
    if unsupported:
        raise ValueError(
            "Unsupported API key scopes: "
            f"{unsupported}. Valid scopes: {allowed}"
        )
    return v

UsersApiKeyCreated

Bases: UsersApiKeyRead

API key creation response schema.

Extends UsersApiKeyRead with the raw key, returned only once at creation time. The raw key is never stored and cannot be retrieved again.

Attributes:

Name Type Description
key StrictStr

The full raw API key. Show to the user once and discard.

Source code in backend/app/users/users_api_keys/schema.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
class UsersApiKeyCreated(UsersApiKeyRead):
    """
    API key creation response schema.

    Extends UsersApiKeyRead with the raw key, returned
    only once at creation time. The raw key is never
    stored and cannot be retrieved again.

    Attributes:
        key: The full raw API key. Show to the user
            once and discard.
    """

    key: StrictStr = Field(
        ...,
        description=(
            "Full raw API key. Shown once at creation " "only — store it securely."
        ),
    )

    model_config = ConfigDict(
        from_attributes=True,
        extra="forbid",
    )

UsersApiKeyRead

Bases: BaseModel

API key read schema for list/detail responses.

Never exposes the raw key or its hash. Safe to return in all authenticated GET responses.

Attributes:

Name Type Description
id StrictStr

Unique key identifier (UUID).

user_id int

Owner's user ID.

name StrictStr

User-friendly label.

key_prefix StrictStr

First 8 chars of the random part.

scopes StrictStr

JSON-encoded list of granted scopes.

expires_at datetime | None

Optional expiration timestamp.

last_used_at datetime | None

Last successful use timestamp.

created_at datetime

Key creation timestamp.

is_active StrictBool

Whether the key is currently active.

Source code in backend/app/users/users_api_keys/schema.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
class UsersApiKeyRead(BaseModel):
    """
    API key read schema for list/detail responses.

    Never exposes the raw key or its hash. Safe to
    return in all authenticated GET responses.

    Attributes:
        id: Unique key identifier (UUID).
        user_id: Owner's user ID.
        name: User-friendly label.
        key_prefix: First 8 chars of the random part.
        scopes: JSON-encoded list of granted scopes.
        expires_at: Optional expiration timestamp.
        last_used_at: Last successful use timestamp.
        created_at: Key creation timestamp.
        is_active: Whether the key is currently active.
    """

    id: StrictStr = Field(..., description="Unique key identifier (UUID)")
    user_id: int = Field(..., ge=1, description="Owner's user ID")
    name: StrictStr = Field(..., description="User-friendly label")
    key_prefix: StrictStr = Field(..., description="First 8 chars of the random part")
    scopes: StrictStr = Field(..., description="JSON-encoded list of granted scopes")
    expires_at: datetime | None = Field(
        None, description="Optional expiration timestamp"
    )
    last_used_at: datetime | None = Field(
        None, description="Last successful use timestamp"
    )
    created_at: datetime = Field(..., description="Key creation timestamp")
    is_active: StrictBool = Field(
        ..., description="Whether the key is currently active"
    )

    model_config = ConfigDict(from_attributes=True)

UsersApiKeysModel

Bases: Base

User API key for third-party integrations.

Attributes:

Name Type Description
id Mapped[str]

Unique key identifier (UUID).

user_id Mapped[int]

Foreign key to users table.

name Mapped[str]

User-friendly label for the key.

key_prefix Mapped[str]

First 8 chars of the random part, shown in UI for identification.

key_hash Mapped[str]

SHA-256 hex digest of the full raw key. Never stored plaintext.

scopes Mapped[str]

JSON-encoded list of granted scopes.

expires_at Mapped[datetime | None]

Optional expiration timestamp. NULL means the key never expires.

last_used_at Mapped[datetime | None]

Timestamp of last successful authentication. Updated on each use.

created_at Mapped[datetime]

Key creation timestamp.

is_active Mapped[bool]

Whether the key is active. Set to False to revoke without deleting.

users Mapped[Users]

Relationship to Users model.

Source code in backend/app/users/users_api_keys/models.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class UsersApiKeys(Base):
    """
    User API key for third-party integrations.

    Attributes:
        id: Unique key identifier (UUID).
        user_id: Foreign key to users table.
        name: User-friendly label for the key.
        key_prefix: First 8 chars of the random part,
            shown in UI for identification.
        key_hash: SHA-256 hex digest of the full raw
            key. Never stored plaintext.
        scopes: JSON-encoded list of granted scopes.
        expires_at: Optional expiration timestamp.
            NULL means the key never expires.
        last_used_at: Timestamp of last successful
            authentication. Updated on each use.
        created_at: Key creation timestamp.
        is_active: Whether the key is active. Set to
            False to revoke without deleting.
        users: Relationship to Users model.
    """

    __tablename__ = "users_api_keys"

    id: Mapped[str] = mapped_column(
        String(36),
        primary_key=True,
        nullable=False,
        comment="Unique key identifier (UUID4)",
    )
    user_id: Mapped[int] = mapped_column(
        ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
        comment="User ID that the API key belongs to",
    )
    name: Mapped[str] = mapped_column(
        String(100),
        nullable=False,
        comment="User-friendly label for the key",
    )
    key_prefix: Mapped[str] = mapped_column(
        String(8),
        nullable=False,
        comment=("First 8 chars of the random key part, " "used for UI identification"),
    )
    key_hash: Mapped[str] = mapped_column(
        String(64),
        nullable=False,
        unique=True,
        index=True,
        comment="SHA-256 hex digest of the full raw key",
    )
    scopes: Mapped[str] = mapped_column(
        Text,
        nullable=False,
        comment="JSON-encoded list of granted scope strings",
    )
    expires_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True),
        nullable=True,
        comment="Key expiration timestamp (NULL = no expiry)",
    )
    last_used_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True),
        nullable=True,
        comment="Timestamp of last successful authentication",
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        nullable=False,
        comment="Key creation timestamp",
    )
    is_active: Mapped[bool] = mapped_column(
        default=True,
        nullable=False,
        comment=("Whether the key is active. False = revoked " "(soft delete)"),
    )

    # Relationship to Users model
    users: Mapped["Users"] = relationship(back_populates="users_api_keys")

create_api_key

create_api_key(user_id, data, db)

Create a new API key for a user.

Generates a cryptographically random key, hashes it with SHA-256, and stores only the hash. Returns both the ORM object and the raw key so the caller can include it in the response (shown once only).

Parameters:

Name Type Description Default
user_id int

The ID of the owning user.

required
data UsersApiKeyCreate

Validated creation schema.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
tuple[UsersApiKeys, str]

Tuple of (UsersApiKeys ORM object, raw key string).

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/users/users_api_keys/crud.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@core_decorators.handle_db_errors
def create_api_key(
    user_id: int,
    data: users_api_keys_schema.UsersApiKeyCreate,
    db: Session,
) -> tuple[
    users_api_keys_models.UsersApiKeys,
    str,
]:
    """
    Create a new API key for a user.

    Generates a cryptographically random key, hashes it
    with SHA-256, and stores only the hash. Returns both
    the ORM object and the raw key so the caller can
    include it in the response (shown once only).

    Args:
        user_id: The ID of the owning user.
        data: Validated creation schema.
        db: SQLAlchemy database session.

    Returns:
        Tuple of (UsersApiKeys ORM object, raw key string).

    Raises:
        HTTPException: If a database error occurs.
    """
    raw_key = users_api_keys_utils.generate_api_key()
    # Prefix is "endurain_" (9 chars) + first 8 chars of
    # the random part
    key_prefix = raw_key[9:17]
    key_hash = users_api_keys_utils.hash_api_key(raw_key)
    scopes_json = users_api_keys_utils.scopes_to_json(data.scopes)

    db_api_key = users_api_keys_models.UsersApiKeys(
        id=str(uuid.uuid4()),
        user_id=user_id,
        name=data.name,
        key_prefix=key_prefix,
        key_hash=key_hash,
        scopes=scopes_json,
        expires_at=data.expires_at,
        last_used_at=None,
        created_at=datetime.now(timezone.utc),
        is_active=True,
    )
    db.add(db_api_key)
    db.commit()
    db.refresh(db_api_key)

    core_logger.print_to_log(
        "API key created",
        "info",
        context={
            "user_id": user_id,
            "key_prefix": key_prefix,
            "name": data.name,
        },
    )

    return db_api_key, raw_key

delete_api_key

delete_api_key(api_key_id, user_id, db)

Permanently delete an API key.

Hard-delete for GDPR-style removal. The key hash is gone and cannot be authenticated against after this.

Parameters:

Name Type Description Default
api_key_id str

The UUID of the API key.

required
user_id int

The ID of the owning user.

required
db Session

SQLAlchemy database session.

required

Raises:

Type Description
HTTPException

If the key is not found or does not belong to the user (404), or a database error occurs.

Source code in backend/app/users/users_api_keys/crud.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
@core_decorators.handle_db_errors
def delete_api_key(
    api_key_id: str,
    user_id: int,
    db: Session,
) -> None:
    """
    Permanently delete an API key.

    Hard-delete for GDPR-style removal. The key hash is
    gone and cannot be authenticated against after this.

    Args:
        api_key_id: The UUID of the API key.
        user_id: The ID of the owning user.
        db: SQLAlchemy database session.

    Raises:
        HTTPException: If the key is not found or does not
            belong to the user (404), or a database error
            occurs.
    """
    db_api_key = get_api_key_by_id(api_key_id, user_id, db)

    if db_api_key is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=(f"API key {api_key_id} not found " f"for user {user_id}"),
        )

    db.delete(db_api_key)
    db.commit()

    core_logger.print_to_log(
        "API key deleted",
        "info",
        context={
            "api_key_id": api_key_id,
            "user_id": user_id,
        },
    )

generate_api_key

generate_api_key()

Generate a new raw API key.

Keys have the format endurain_<token> where <token> is 32 cryptographically random bytes encoded as base64url (43 characters). Total entropy is 256 bits.

Returns:

Type Description
str

A new raw API key string.

Source code in backend/app/users/users_api_keys/utils.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def generate_api_key() -> str:
    """
    Generate a new raw API key.

    Keys have the format ``endurain_<token>`` where
    ``<token>`` is 32 cryptographically random bytes
    encoded as base64url (43 characters). Total entropy
    is 256 bits.

    Returns:
        A new raw API key string.
    """
    return f"endurain_{secrets.token_urlsafe(32)}"

get_api_key_by_hash

get_api_key_by_hash(key_hash, db)

Retrieve an API key by its SHA-256 hash.

Used during request authentication to look up the key record from the hashed incoming value.

Parameters:

Name Type Description Default
key_hash str

SHA-256 hex digest of the raw key.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
UsersApiKeys | None

The API key object if found, None otherwise.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/users/users_api_keys/crud.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@core_decorators.handle_db_errors
def get_api_key_by_hash(
    key_hash: str,
    db: Session,
) -> users_api_keys_models.UsersApiKeys | None:
    """
    Retrieve an API key by its SHA-256 hash.

    Used during request authentication to look up the
    key record from the hashed incoming value.

    Args:
        key_hash: SHA-256 hex digest of the raw key.
        db: SQLAlchemy database session.

    Returns:
        The API key object if found, None otherwise.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(users_api_keys_models.UsersApiKeys).where(
        users_api_keys_models.UsersApiKeys.key_hash == key_hash
    )
    return db.execute(stmt).scalar_one_or_none()

get_api_key_by_id

get_api_key_by_id(api_key_id, user_id, db)

Retrieve a single API key by its ID and owner.

Parameters:

Name Type Description Default
api_key_id str

The UUID of the API key.

required
user_id int

The ID of the owning user.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
UsersApiKeys | None

The API key object if found, None otherwise.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/users/users_api_keys/crud.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@core_decorators.handle_db_errors
def get_api_key_by_id(
    api_key_id: str,
    user_id: int,
    db: Session,
) -> users_api_keys_models.UsersApiKeys | None:
    """
    Retrieve a single API key by its ID and owner.

    Args:
        api_key_id: The UUID of the API key.
        user_id: The ID of the owning user.
        db: SQLAlchemy database session.

    Returns:
        The API key object if found, None otherwise.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(users_api_keys_models.UsersApiKeys).where(
        users_api_keys_models.UsersApiKeys.id == api_key_id,
        users_api_keys_models.UsersApiKeys.user_id == user_id,
    )
    return db.execute(stmt).scalar_one_or_none()

get_api_keys_by_user_id

get_api_keys_by_user_id(user_id, db)

Retrieve all API keys for a user.

Parameters:

Name Type Description Default
user_id int

The ID of the owning user.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
list[UsersApiKeys]

List of API key objects ordered by creation

list[UsersApiKeys]

date descending.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/users/users_api_keys/crud.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@core_decorators.handle_db_errors
def get_api_keys_by_user_id(
    user_id: int,
    db: Session,
) -> list[users_api_keys_models.UsersApiKeys]:
    """
    Retrieve all API keys for a user.

    Args:
        user_id: The ID of the owning user.
        db: SQLAlchemy database session.

    Returns:
        List of API key objects ordered by creation
        date descending.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = (
        select(users_api_keys_models.UsersApiKeys)
        .where(users_api_keys_models.UsersApiKeys.user_id == user_id)
        .order_by(users_api_keys_models.UsersApiKeys.created_at.desc())
    )
    return list(db.execute(stmt).scalars().all())

hash_api_key

hash_api_key(raw_key)

Compute the SHA-256 hex digest of a raw API key.

High-entropy secrets do not require a slow KDF (Argon2/bcrypt). SHA-256 is the industry standard for hashing tokens of this entropy level.

Parameters:

Name Type Description Default
raw_key str

The plain-text API key to hash.

required

Returns:

Type Description
str

Lowercase hex-encoded SHA-256 digest (64 chars).

Source code in backend/app/users/users_api_keys/utils.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def hash_api_key(raw_key: str) -> str:
    """
    Compute the SHA-256 hex digest of a raw API key.

    High-entropy secrets do not require a slow KDF
    (Argon2/bcrypt). SHA-256 is the industry standard
    for hashing tokens of this entropy level.

    Args:
        raw_key: The plain-text API key to hash.

    Returns:
        Lowercase hex-encoded SHA-256 digest (64 chars).
    """
    return hashlib.sha256(raw_key.encode()).hexdigest()

revoke_api_key

revoke_api_key(api_key_id, user_id, db)

Revoke an API key by setting is_active to False.

Soft-delete: the record is retained for audit purposes but the key will be rejected on next use.

Parameters:

Name Type Description Default
api_key_id str

The UUID of the API key.

required
user_id int

The ID of the owning user.

required
db Session

SQLAlchemy database session.

required

Raises:

Type Description
HTTPException

If the key is not found or does not belong to the user (404), or a database error occurs.

Source code in backend/app/users/users_api_keys/crud.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
@core_decorators.handle_db_errors
def revoke_api_key(
    api_key_id: str,
    user_id: int,
    db: Session,
) -> None:
    """
    Revoke an API key by setting is_active to False.

    Soft-delete: the record is retained for audit purposes
    but the key will be rejected on next use.

    Args:
        api_key_id: The UUID of the API key.
        user_id: The ID of the owning user.
        db: SQLAlchemy database session.

    Raises:
        HTTPException: If the key is not found or does not
            belong to the user (404), or a database error
            occurs.
    """
    db_api_key = get_api_key_by_id(api_key_id, user_id, db)

    if db_api_key is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=(f"API key {api_key_id} not found " f"for user {user_id}"),
        )

    db_api_key.is_active = False
    db.commit()

    core_logger.print_to_log(
        "API key revoked",
        "info",
        context={
            "api_key_id": api_key_id,
            "user_id": user_id,
        },
    )

update_last_used

update_last_used(api_key_id, db)

Update the last_used_at timestamp for an API key.

Parameters:

Name Type Description Default
api_key_id str

The UUID of the API key.

required
db Session

SQLAlchemy database session.

required

Raises:

Type Description
HTTPException

If the key is not found (404) or a database error occurs.

Source code in backend/app/users/users_api_keys/crud.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
@core_decorators.handle_db_errors
def update_last_used(
    api_key_id: str,
    db: Session,
) -> None:
    """
    Update the last_used_at timestamp for an API key.

    Args:
        api_key_id: The UUID of the API key.
        db: SQLAlchemy database session.

    Raises:
        HTTPException: If the key is not found (404) or
            a database error occurs.
    """
    stmt = select(users_api_keys_models.UsersApiKeys).where(
        users_api_keys_models.UsersApiKeys.id == api_key_id
    )
    db_api_key = db.execute(stmt).scalar_one_or_none()

    if db_api_key is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"API key {api_key_id} not found",
        )

    db_api_key.last_used_at = datetime.now(timezone.utc)
    db.commit()

validate_api_key_scopes

validate_api_key_scopes(requested_scopes, _user_access_type)

Validate requested scopes against supported API-key scopes.

API keys currently support only activity uploads. Keeping this allow-list separate from the full JWT scope set prevents API keys from silently gaining access when new unified-auth endpoints are added later.

Parameters:

Name Type Description Default
requested_scopes list[str]

List of scopes the caller wants to assign to the new API key.

required
_user_access_type str

The owner's access type. Accepted for router compatibility but not used while API-key scopes are restricted to activity uploads.

required

Raises:

Type Description
ValueError

If any requested scope is not supported.

Source code in backend/app/users/users_api_keys/utils.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def validate_api_key_scopes(
    requested_scopes: list[str],
    _user_access_type: str,
) -> None:
    """
    Validate requested scopes against supported API-key scopes.

    API keys currently support only activity uploads. Keeping this
    allow-list separate from the full JWT scope set prevents API keys
    from silently gaining access when new unified-auth endpoints are
    added later.

    Args:
        requested_scopes: List of scopes the caller wants
            to assign to the new API key.
        _user_access_type: The owner's access type. Accepted for
            router compatibility but not used while API-key scopes
            are restricted to activity uploads.

    Raises:
        ValueError: If any requested scope is not supported.
    """
    allowed = {"activities:upload"}
    unsupported = set(requested_scopes) - allowed
    if unsupported or not requested_scopes:
        raise ValueError(
            "Unsupported API key scopes: "
            f"{unsupported}. Valid scopes: {allowed}"
        )