Skip to content
1 change: 1 addition & 0 deletions changes/13777.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Return the login user's id and rate limit from the authorization API, and keep both in the web server session.
14 changes: 9 additions & 5 deletions src/ai/backend/common/dto/manager/auth/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from enum import StrEnum
from typing import Any, Self

from ai.backend.common.types import BackendAISchema
from ai.backend.common.data.user.types import UserRole
from ai.backend.common.identifier.user import UserID
from ai.backend.common.types import AccessKey, BackendAISchema, SecretKey

__all__ = (
"AuthTokenType",
Expand Down Expand Up @@ -50,14 +52,16 @@ def parse(cls, data: dict[str, Any]) -> Self:
class AuthSuccessResponse(AuthResponse):
"""Returned when authorization succeeds without requiring 2FA."""

access_key: str
secret_key: str
role: str
access_key: AccessKey
secret_key: SecretKey
role: UserRole
status: str
session_token: str
user_id: UserID
rate_limit: int | None
Comment thread
jopemachine marked this conversation as resolved.
Outdated
type: AuthTokenType = AuthTokenType.KEYPAIR

def to_dict(self) -> dict[str, str]:
def to_dict(self) -> dict[str, Any]:
return self.model_dump(mode="json")


Expand Down
2 changes: 2 additions & 0 deletions src/ai/backend/manager/api/rest/auth/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ async def authorize(
role=auth_result.role,
status=auth_result.status,
session_token=auth_result.session_token,
user_id=auth_result.user_id,
rate_limit=auth_result.rate_limit,
)
resp = AuthorizeResponse(data=data)
return APIResponse.build(HTTPStatus.OK, resp)
Expand Down
9 changes: 5 additions & 4 deletions src/ai/backend/manager/data/auth/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ class SSHKeypair:

@dataclass
class AuthorizationResult:
user_id: uuid.UUID
access_key: str
secret_key: str
role: str
user_id: UserID
access_key: AccessKey
secret_key: SecretKey
role: UserRole
status: str
session_token: str
rate_limit: int | None


@dataclass
Expand Down
15 changes: 9 additions & 6 deletions src/ai/backend/manager/services/auth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
)
from ai.backend.common.dto.manager.auth.types import AuthTokenType
from ai.backend.common.exception import InvalidAPIParameters, UserResourcePolicyNotFound
from ai.backend.common.identifier.user import UserID
from ai.backend.common.plugin.hook import ALL_COMPLETED, FIRST_COMPLETED, PASSED, HookPluginContext
from ai.backend.common.types import AccessKey, SSHPrivateKey, SSHPublicKey
from ai.backend.common.types import AccessKey, SecretKey, SSHPrivateKey, SSHPublicKey
from ai.backend.logging.utils import BraceStyleAdapter
from ai.backend.manager.config.provider import ManagerConfigProvider
from ai.backend.manager.config.unified import AuthConfig
Expand Down Expand Up @@ -394,7 +395,7 @@ async def _create_login_session(
token=LoginSessionTokenData(
type="keypair",
access_key=keypair_row.access_key,
secret_key=keypair_row.secret_key or "",
secret_key=keypair_row.secret_key,
role=user.role,
status=user.status,
),
Expand All @@ -409,12 +410,14 @@ async def _create_login_session(
return AuthorizeActionResult(
stream_response=None,
authorization_result=AuthorizationResult(
access_key=keypair_row.access_key,
secret_key=keypair_row.secret_key or "",
user_id=user.uuid,
role=user.role,
access_key=AccessKey(keypair_row.access_key),
secret_key=SecretKey(keypair_row.secret_key),
user_id=UserID(user.uuid),
role=UserRole(user.role),
status=user.status,
session_token=session_result.session_token,
# TODO: take this from the user resource policy instead of the keypair.
rate_limit=keypair_row.rate_limit,
),
)

Expand Down
4 changes: 4 additions & 0 deletions src/ai/backend/web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ async def _set_login_history(last_login_attempt: float, login_fail_count: float
"secret_key": token.secret_key,
"role": token.role,
"status": token.status,
"user_id": str(token.user_id),
"rate_limit": token.rate_limit,
}
public_return = {
"access_key": token.access_key,
Expand Down Expand Up @@ -767,6 +769,8 @@ async def token_login_handler(request: web.Request) -> web.Response:
"secret_key": token.secret_key,
"role": token.role,
"status": token.status,
"user_id": str(token.user_id),
"rate_limit": token.rate_limit,
}
public_return = {
"access_key": token.access_key,
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/client_v2/test_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ async def test_authorize(self, sample_client_type_id: UUID) -> None:
"role": "admin",
"status": "active",
"session_token": "test_session_token",
"user_id": "12345678-1234-5678-1234-567812345678",
"rate_limit": None,
"type": AuthTokenType.KEYPAIR,
},
}
Expand Down
13 changes: 10 additions & 3 deletions tests/unit/common/dto/manager/auth/test_auth_response.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

from uuid import uuid4

from ai.backend.common.api_handlers import BaseResponseModel
from ai.backend.common.data.user.types import UserRole
from ai.backend.common.dto.manager.auth.response import (
AuthorizeResponse,
GetRoleResponse,
Expand All @@ -18,16 +21,20 @@
AuthSuccessResponse,
AuthTokenType,
)
from ai.backend.common.identifier.user import UserID
from ai.backend.common.types import AccessKey, SecretKey


def test_authorize_response() -> None:
data = AuthSuccessResponse(
response_type=AuthResponseType.SUCCESS,
access_key="AKIAIOSFODNN7EXAMPLE",
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
role="user",
access_key=AccessKey("AKIAIOSFODNN7EXAMPLE"),
secret_key=SecretKey("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
role=UserRole.USER,
status="active",
session_token="test_session_token",
user_id=UserID(uuid4()),
rate_limit=None,
type=AuthTokenType.KEYPAIR,
)
resp = AuthorizeResponse(data=data)
Expand Down
25 changes: 19 additions & 6 deletions tests/unit/common/dto/manager/auth/test_auth_types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from uuid import uuid4

from ai.backend.common.data.user.types import UserRole
from ai.backend.common.dto.manager.auth.types import (
AuthResponseType,
AuthSuccessResponse,
Expand All @@ -9,6 +12,8 @@
TwoFactorType,
parse_auth_response,
)
from ai.backend.common.identifier.user import UserID
from ai.backend.common.types import AccessKey, SecretKey


def test_auth_token_type_values() -> None:
Expand All @@ -34,11 +39,13 @@ def test_two_factor_type_values() -> None:
def test_auth_success_response_creation() -> None:
resp = AuthSuccessResponse(
response_type=AuthResponseType.SUCCESS,
access_key="AKTEST",
secret_key="SKTEST",
role="user",
access_key=AccessKey("AKTEST"),
secret_key=SecretKey("SKTEST"),
role=UserRole.USER,
status="active",
session_token="test_session_token",
user_id=UserID(uuid4()),
rate_limit=None,
)
assert resp.access_key == "AKTEST"
assert resp.secret_key == "SKTEST"
Expand All @@ -50,11 +57,13 @@ def test_auth_success_response_creation() -> None:
def test_auth_success_response_to_dict() -> None:
resp = AuthSuccessResponse(
response_type=AuthResponseType.SUCCESS,
access_key="AK",
secret_key="SK",
role="admin",
access_key=AccessKey("AK"),
secret_key=SecretKey("SK"),
role=UserRole.ADMIN,
status="active",
session_token="test_session_token",
user_id=UserID(uuid4()),
rate_limit=None,
type=AuthTokenType.JWT,
)
d = resp.to_dict()
Expand Down Expand Up @@ -115,6 +124,8 @@ def test_parse_auth_response_success() -> None:
"role": "user",
"status": "active",
"session_token": "test_token",
"user_id": "12345678-1234-5678-1234-567812345678",
"rate_limit": None,
}
result = parse_auth_response(data)
assert isinstance(result, AuthSuccessResponse)
Expand Down Expand Up @@ -150,6 +161,8 @@ def test_parse_auth_response_explicit_success() -> None:
"role": "user",
"status": "active",
"session_token": "test_token",
"user_id": "12345678-1234-5678-1234-567812345678",
"rate_limit": None,
}
result = parse_auth_response(data)
assert isinstance(result, AuthSuccessResponse)
Expand Down
9 changes: 6 additions & 3 deletions tests/unit/manager/api/auth/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
UploadSSHKeypairRequest,
VerifyAuthRequest,
)
from ai.backend.common.identifier.user import UserID
from ai.backend.common.types import AccessKey, SecretKey
from ai.backend.manager.api.rest.auth.handler import AuthHandler
from ai.backend.manager.api.rest.middleware.auth import (
TRUSTED_PROXY_NETWORKS_KEY,
Expand Down Expand Up @@ -240,12 +242,13 @@ def authorize_result(self) -> AuthorizeActionResult:
return AuthorizeActionResult(
stream_response=None,
authorization_result=AuthorizationResult(
user_id=uuid.uuid4(),
access_key="TESTKEY",
secret_key="TESTSECRET",
user_id=UserID(uuid.uuid4()),
access_key=AccessKey("TESTKEY"),
secret_key=SecretKey("TESTSECRET"),
role=UserRole.USER,
status=UserStatus.ACTIVE,
session_token="test_session_token",
rate_limit=30000,
),
)

Expand Down
Loading