Skip to content
1 change: 1 addition & 0 deletions changes/9719.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add REST endpoints and GraphQL schema for login session management (GET /login-sessions, DELETE /login-sessions/{id}, my_login_sessions query, update_user_login_security_policy and revoke_login_session mutations).
76 changes: 76 additions & 0 deletions docs/manager/graphql-reference/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5854,6 +5854,39 @@ enum LivenessStatus
DEGRADED @join__enumValue(graph: STRAWBERRY)
}

"""Added in 26.3.0. Login security policy settings for a user."""
type LoginSecurityPolicyGQL
@join__type(graph: STRAWBERRY)
{
"""
Maximum number of concurrent login sessions allowed. None means unlimited.
"""
maxConcurrentLogins: Int
}

"""Added in 26.3.0. Represents an active login session for a user."""
type LoginSessionGQL
@join__type(graph: STRAWBERRY)
{
"""Unique identifier of the login session."""
id: UUID!

"""Opaque session token."""
sessionToken: String!

"""IP address of the client that created the session."""
clientIp: String

"""Timestamp when the session was created."""
createdAt: DateTime!

"""Timestamp when the session expires, or None if no expiry."""
expiredAt: DateTime

"""Reason for session creation or revocation, if any."""
reason: String
}

"""Added in 25.6.0. A pair of timestamp and value."""
type MetricResultValue
@join__type(graph: GRAPHENE)
Expand Down Expand Up @@ -7510,6 +7543,16 @@ type Mutation

"""Added in 26.3.0. Revoke a role from a user (admin only)."""
adminRevokeRole(input: RevokeRoleInput!): RoleAssignment! @join__field(graph: STRAWBERRY)

"""
Added in 26.3.0. Update the login security policy for a user (admin only). Requires superadmin privileges.
"""
updateUserLoginSecurityPolicy(userId: UUID!, input: UpdateLoginSecurityPolicyInput!): UpdateUserLoginSecurityPolicyPayload! @join__field(graph: STRAWBERRY)

"""
Added in 26.3.0. Revoke a specific login session. Users may revoke their own sessions; admins may revoke any session.
"""
revokeLoginSession(sessionId: UUID!): RevokeLoginSessionPayload! @join__field(graph: STRAWBERRY)
}

"""Added in 24.12.0."""
Expand Down Expand Up @@ -9517,6 +9560,11 @@ type Query
"""List route history (superadmin only)"""
routeHistories(filter: RouteHistoryFilter = null, orderBy: [RouteHistoryOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): RouteHistoryConnection @join__field(graph: STRAWBERRY) @deprecated(reason: "Use admin_route_histories instead. This API will be removed after v26.3.0. See BEP-1041 for migration guide.")

"""
Added in 26.3.0. List all active login sessions for the currently authenticated user.
"""
myLoginSessions: [LoginSessionGQL!]! @join__field(graph: STRAWBERRY)

"""
Added in 26.2.0. Get a single user by UUID (admin only). Requires superadmin privileges. Returns an error if user is not found.
"""
Expand Down Expand Up @@ -10245,6 +10293,14 @@ type RestoreArtifactsPayload
artifacts: [Artifact!]!
}

"""Added in 26.3.0. Payload for revokeLoginSession mutation."""
type RevokeLoginSessionPayload
@join__type(graph: STRAWBERRY)
{
"""Whether the session was successfully revoked."""
success: Boolean!
}

"""Added in 26.3.0. Input for revoking a role from a user"""
input RevokeRoleInput
@join__type(graph: STRAWBERRY)
Expand Down Expand Up @@ -11923,6 +11979,18 @@ type UpdateHuggingFaceRegistryPayload
huggingfaceRegistry: HuggingFaceRegistry!
}

"""
Added in 26.3.0. Input for updating a user's login security policy. max_concurrent_logins must be a positive integer or None (unlimited).
"""
input UpdateLoginSecurityPolicyInput
@join__type(graph: STRAWBERRY)
{
"""
Maximum number of concurrent login sessions allowed. Must be a positive integer (greater than 0), or None for unlimited. Zero and negative values are rejected.
"""
maxConcurrentLogins: Int = null
}

"""Input for updating a notification channel"""
input UpdateNotificationChannelInput
@join__type(graph: STRAWBERRY)
Expand Down Expand Up @@ -12113,6 +12181,14 @@ type UpdateRouteTrafficStatusPayload
route: Route!
}

"""Added in 26.3.0. Payload for updateUserLoginSecurityPolicy mutation."""
type UpdateUserLoginSecurityPolicyPayload
@join__type(graph: STRAWBERRY)
{
"""The updated login security policy."""
loginSecurityPolicy: LoginSecurityPolicyGQL!
}

"""
Added in 26.3.0. Input for updating user information. All fields are optional - only provided fields will be updated.
"""
Expand Down
66 changes: 66 additions & 0 deletions docs/manager/graphql-reference/v2-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3231,6 +3231,35 @@ enum LivenessStatus {
DEGRADED
}

"""Added in 26.3.0. Login security policy settings for a user."""
type LoginSecurityPolicyGQL {
"""
Maximum number of concurrent login sessions allowed. None means unlimited.
"""
maxConcurrentLogins: Int
}

"""Added in 26.3.0. Represents an active login session for a user."""
type LoginSessionGQL {
"""Unique identifier of the login session."""
id: UUID!

"""Opaque session token."""
sessionToken: String!

"""IP address of the client that created the session."""
clientIp: String

"""Timestamp when the session was created."""
createdAt: DateTime!

"""Timestamp when the session expires, or None if no expiry."""
expiredAt: DateTime

"""Reason for session creation or revocation, if any."""
reason: String
}

type ModelDeployment implements Node {
"""The Globally Unique ID of this object"""
id: ID!
Expand Down Expand Up @@ -3943,6 +3972,16 @@ type Mutation {

"""Added in 26.3.0. Revoke a role from a user (admin only)."""
adminRevokeRole(input: RevokeRoleInput!): RoleAssignment!

"""
Added in 26.3.0. Update the login security policy for a user (admin only). Requires superadmin privileges.
"""
updateUserLoginSecurityPolicy(userId: UUID!, input: UpdateLoginSecurityPolicyInput!): UpdateUserLoginSecurityPolicyPayload!

"""
Added in 26.3.0. Revoke a specific login session. Users may revoke their own sessions; admins may revoke any session.
"""
revokeLoginSession(sessionId: UUID!): RevokeLoginSessionPayload!
}

"""An object with a Globally Unique ID"""
Expand Down Expand Up @@ -5244,6 +5283,11 @@ type Query {
"""List route history (superadmin only)"""
routeHistories(filter: RouteHistoryFilter = null, orderBy: [RouteHistoryOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): RouteHistoryConnection @deprecated(reason: "Use admin_route_histories instead. This API will be removed after v26.3.0. See BEP-1041 for migration guide.")

"""
Added in 26.3.0. List all active login sessions for the currently authenticated user.
"""
myLoginSessions: [LoginSessionGQL!]!

"""
Added in 26.2.0. Get a single user by UUID (admin only). Requires superadmin privileges. Returns an error if user is not found.
"""
Expand Down Expand Up @@ -5886,6 +5930,12 @@ type RestoreArtifactsPayload {
artifacts: [Artifact!]!
}

"""Added in 26.3.0. Payload for revokeLoginSession mutation."""
type RevokeLoginSessionPayload {
"""Whether the session was successfully revoked."""
success: Boolean!
}

"""Added in 26.3.0. Input for revoking a role from a user"""
input RevokeRoleInput {
userId: UUID!
Expand Down Expand Up @@ -6999,6 +7049,16 @@ type UpdateHuggingFaceRegistryPayload {
huggingfaceRegistry: HuggingFaceRegistry!
}

"""
Added in 26.3.0. Input for updating a user's login security policy. max_concurrent_logins must be a positive integer or None (unlimited).
"""
input UpdateLoginSecurityPolicyInput {
"""
Maximum number of concurrent login sessions allowed. Must be a positive integer (greater than 0), or None for unlimited. Zero and negative values are rejected.
"""
maxConcurrentLogins: Int = null
}

"""Input for updating a notification channel"""
input UpdateNotificationChannelInput {
id: ID!
Expand Down Expand Up @@ -7159,6 +7219,12 @@ type UpdateRouteTrafficStatusPayload {
route: Route!
}

"""Added in 26.3.0. Payload for updateUserLoginSecurityPolicy mutation."""
type UpdateUserLoginSecurityPolicyPayload {
"""The updated login security policy."""
loginSecurityPolicy: LoginSecurityPolicyGQL!
}

"""
Added in 26.3.0. Input for updating user information. All fields are optional - only provided fields will be updated.
"""
Expand Down
7 changes: 7 additions & 0 deletions src/ai/backend/common/dto/manager/login_session/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Common DTOs for login session management used by the Manager.

Import directly from submodules:
- request: RevokeLoginSessionRequest, UpdateLoginSecurityPolicyRequest
- response: LoginSessionItemResponse, ListLoginSessionsResponse
"""
33 changes: 33 additions & 0 deletions src/ai/backend/common/dto/manager/login_session/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
Request DTOs for login session management.
Used by Manager REST API endpoints.
"""

from __future__ import annotations

from pydantic import Field

from ai.backend.common.api_handlers import BaseRequestModel

__all__ = (
"RevokeLoginSessionRequest",
"UpdateLoginSecurityPolicyRequest",
)


class RevokeLoginSessionRequest(BaseRequestModel):
"""Request to revoke a specific login session."""

session_id: str = Field(
description="Unique identifier of the login session to revoke",
)


class UpdateLoginSecurityPolicyRequest(BaseRequestModel):
"""Request to update the login security policy for a user."""

max_concurrent_logins: int | None = Field(
default=None,
gt=0,
description="Maximum number of concurrent login sessions allowed; None means unlimited",
)
45 changes: 45 additions & 0 deletions src/ai/backend/common/dto/manager/login_session/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Response DTOs for login session management.
Used by Manager REST API endpoints.
"""

from __future__ import annotations

from datetime import datetime

from pydantic import Field

from ai.backend.common.api_handlers import BaseResponseModel

__all__ = (
"LoginSessionItemResponse",
"ListLoginSessionsResponse",
)


class LoginSessionItemResponse(BaseResponseModel):
"""A single login session entry."""

id: str = Field(description="Unique identifier of the login session")
session_token: str = Field(description="Opaque token identifying this session")
client_ip: str | None = Field(
default=None,
description="IP address of the client that created this session",
)
created_at: datetime = Field(description="Timestamp when the session was created")
expired_at: datetime | None = Field(
default=None,
description="Timestamp when the session expires; None if it does not expire",
)
reason: str | None = Field(
default=None,
description="Human-readable reason for the session state (e.g., revocation reason)",
)


class ListLoginSessionsResponse(BaseResponseModel):
"""Response containing a list of login sessions for the current user."""

items: list[LoginSessionItemResponse] = Field(
description="List of active login sessions",
)
2 changes: 2 additions & 0 deletions src/ai/backend/common/dto/manager/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .request import (
CreateUserRequest,
DeleteUserRequest,
LoginSecurityPolicyRequest,
PurgeUserRequest,
SearchUsersRequest,
UpdateUserRequest,
Expand Down Expand Up @@ -38,6 +39,7 @@
"UserStatus",
# Request DTOs
"CreateUserRequest",
"LoginSecurityPolicyRequest",
"UpdateUserRequest",
"SearchUsersRequest",
"DeleteUserRequest",
Expand Down
25 changes: 24 additions & 1 deletion src/ai/backend/common/dto/manager/user/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from uuid import UUID

from pydantic import Field
from pydantic import Field, field_validator

from ai.backend.common.api_handlers import BaseRequestModel
from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter
Expand All @@ -17,6 +17,7 @@
__all__ = (
"CreateUserRequest",
"DeleteUserRequest",
"LoginSecurityPolicyRequest",
"PurgeUserRequest",
"SearchUsersRequest",
"UpdateUserRequest",
Expand All @@ -25,6 +26,25 @@
)


class LoginSecurityPolicyRequest(BaseRequestModel):
"""Login security policy settings for a user."""

max_concurrent_logins: int | None = Field(
default=None,
description=(
"Maximum number of concurrent login sessions allowed. "
"Must be a positive integer (greater than 0), or None for unlimited."
),
)

@field_validator("max_concurrent_logins")
@classmethod
def validate_max_concurrent_logins(cls, v: int | None) -> int | None:
if v is not None and v <= 0:
raise ValueError("max_concurrent_logins must be a positive integer or None")
return v


class CreateUserRequest(BaseRequestModel):
"""Request to create a new user."""

Expand Down Expand Up @@ -81,6 +101,9 @@ class UpdateUserRequest(BaseRequestModel):
default=None, description="Updated container additional GIDs"
)
group_ids: list[str] | None = Field(default=None, description="Updated group IDs")
login_security_policy: LoginSecurityPolicyRequest | None = Field(
default=None, description="Login security policy settings (e.g. max_concurrent_logins)"
)


class UserFilter(BaseRequestModel):
Expand Down
Loading
Loading