Bladeren bron

feat: add auth service

Jax Docker 1 maand geleden
bovenliggende
commit
83d1f3493e
31 gewijzigde bestanden met toevoegingen van 1006 en 0 verwijderingen
  1. 34 0
      README.md
  2. 24 0
      deployments/docker/docker-compose.yml
  3. 18 0
      libs/core-domain/src/core_domain/__init__.py
  4. 60 0
      libs/core-domain/src/core_domain/auth_contracts.py
  5. 27 0
      services/api-gateway/app/api/routes.py
  6. 1 0
      services/api-gateway/app/bootstrap/settings.py
  7. 1 0
      services/api-gateway/app/infrastructure/proxy.py
  8. 37 0
      services/auth-service/alembic.ini
  9. 38 0
      services/auth-service/alembic/env.py
  10. 1 0
      services/auth-service/alembic/versions/.gitkeep
  11. 94 0
      services/auth-service/alembic/versions/20260425_0001_init_auth_models.py
  12. 1 0
      services/auth-service/app/__init__.py
  13. 1 0
      services/auth-service/app/api/__init__.py
  14. 134 0
      services/auth-service/app/api/routes.py
  15. 1 0
      services/auth-service/app/application/__init__.py
  16. 147 0
      services/auth-service/app/application/services.py
  17. 1 0
      services/auth-service/app/bootstrap/__init__.py
  18. 17 0
      services/auth-service/app/bootstrap/app.py
  19. 7 0
      services/auth-service/app/bootstrap/settings.py
  20. 1 0
      services/auth-service/app/db/__init__.py
  21. 7 0
      services/auth-service/app/db/models/__init__.py
  22. 15 0
      services/auth-service/app/db/models/role.py
  23. 17 0
      services/auth-service/app/db/models/role_assignment.py
  24. 19 0
      services/auth-service/app/db/models/user.py
  25. 28 0
      services/auth-service/app/db/session.py
  26. 1 0
      services/auth-service/app/domain/__init__.py
  27. 160 0
      services/auth-service/app/domain/repositories.py
  28. 3 0
      services/auth-service/app/main.py
  29. 1 0
      services/auth-service/app/schemas/__init__.py
  30. 85 0
      services/auth-service/app/schemas/auth.py
  31. 25 0
      services/auth-service/pyproject.toml

+ 34 - 0
README.md

@@ -23,6 +23,7 @@
 - `human-service`
 - `knowledge-service`
 - `event-service`
+- `auth-service`
 - `tool-service`
 
 每个服务都提供了最小 `FastAPI` 启动入口和健康检查接口,数据库相关服务也已经带上了 `SQLAlchemy` 模型骨架与 Alembic 目录。
@@ -59,6 +60,7 @@ pip install -e .\services\skill-service
 pip install -e .\services\human-service
 pip install -e .\services\knowledge-service
 pip install -e .\services\event-service
+pip install -e .\services\auth-service
 pip install -e .\services\tool-service
 ```
 
@@ -206,6 +208,7 @@ services/
   human-service/
   knowledge-service/
   event-service/
+  auth-service/
   tool-service/
 libs/
   core-domain/
@@ -515,6 +518,35 @@ Invoke-RestMethod -Method Post `
 
 Through `api-gateway`, use `/gateway/events/**`.
 
+## Auth Service APIs
+
+`auth-service` stores users, roles, role assignments, and permission checks.
+This is the first RBAC layer for tenant governance.
+
+```powershell
+$user = Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8014/auth/users `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","username":"alice","display_name":"Alice"}'
+
+$role = Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8014/auth/roles `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","code":"admin","name":"Admin","permissions_json":["*"]}'
+
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8014/auth/assignments `
+  -ContentType "application/json" `
+  -Body "{`"tenant_id`":`"t1`",`"user_id`":`"$($user.id)`",`"role_id`":`"$($role.id)`"}"
+
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8014/auth/permissions/check `
+  -ContentType "application/json" `
+  -Body "{`"tenant_id`":`"t1`",`"user_id`":`"$($user.id)`",`"permission`":`"workflow:write`"}"
+```
+
+Through `api-gateway`, use `/gateway/auth/**`.
+
 Execute an agent run without calling an external model:
 
 ```powershell
@@ -890,6 +922,7 @@ $env:AGENT_PLATFORM_SMOKE_RUNTIME_URL="http://127.0.0.1:8000/gateway/runtime"
 - `/gateway/human/**` -> `human-service /human/**`
 - `/gateway/knowledge/**` -> `knowledge-service /knowledge/**`
 - `/gateway/events/**` -> `event-service /events/**`
+- `/gateway/auth/**` -> `auth-service /auth/**`
 - `/gateway/tools/**` -> `tool-service /tools/**`
 - `/gateway/models/**` -> `model-gateway-service /models/**`
 - `/gateway/code/**` -> `code-runner-service /code/**`
@@ -1135,6 +1168,7 @@ Important notes:
 - `human-service` stores human approval, input, pause/resume, and takeover task records under `/data`
 - `knowledge-service` stores knowledge bases, documents, chunks, and local retrieval metadata under `/data`
 - `event-service` stores platform events and delivery status under `/data`
+- `auth-service` stores users, roles, assignments, and permission policy metadata under `/data`
 - `agent-worker` has no exposed port and can be scaled independently; set `AGENT_PLATFORM_AGENT_WORKER_DRY_RUN=true` for no-key local smoke runs
 - `runtime-worker` has no exposed port and can be scaled independently; prefer PostgreSQL for real multi-worker write concurrency
 - `runtime-service` automatically resolves internal URLs to `workflow-service`, `tool-service`, `model-gateway-service`, and `code-runner-service`

+ 24 - 0
deployments/docker/docker-compose.yml

@@ -313,6 +313,26 @@ services:
       timeout: 5s
       retries: 5
 
+  auth-service:
+    build:
+      context: ../..
+      dockerfile: deployments/docker/python-service.Dockerfile
+      args:
+        SERVICE_PATH: services/auth-service
+    container_name: agent-platform-auth-service
+    command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8014"]
+    environment:
+      AGENT_PLATFORM_DATABASE_URL: sqlite:////data/auth_service.db
+    ports:
+      - "8014:8014"
+    volumes:
+      - auth_service_data:/data
+    healthcheck:
+      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8014/auth/health').read()"]
+      interval: 15s
+      timeout: 5s
+      retries: 5
+
   runtime-service:
     build:
       context: ../..
@@ -423,6 +443,7 @@ services:
       AGENT_PLATFORM_HUMAN_SERVICE_URL: http://human-service:8011
       AGENT_PLATFORM_KNOWLEDGE_SERVICE_URL: http://knowledge-service:8012
       AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
+      AGENT_PLATFORM_AUTH_SERVICE_URL: http://auth-service:8014
       AGENT_PLATFORM_AUTH_REQUIRED: ${AGENT_PLATFORM_AUTH_REQUIRED:-false}
     ports:
       - "8000:8000"
@@ -455,6 +476,8 @@ services:
         condition: service_started
       event-service:
         condition: service_started
+      auth-service:
+        condition: service_started
     healthcheck:
       test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health').read()"]
       interval: 15s
@@ -470,6 +493,7 @@ volumes:
   human_service_data:
   knowledge_service_data:
   event_service_data:
+  auth_service_data:
   workflow_service_data:
   session_service_data:
   runtime_service_data:

+ 18 - 0
libs/core-domain/src/core_domain/__init__.py

@@ -10,6 +10,16 @@ from .agent_contracts import (
     AgentVersionContract,
     AgentVersionStatus,
 )
+from .auth_contracts import (
+    PermissionCheckContract,
+    PermissionCheckResultContract,
+    RoleAssignmentContract,
+    RoleAssignmentStatus,
+    RoleContract,
+    RoleStatus,
+    UserContract,
+    UserStatus,
+)
 from .code_contracts import CodeExecutionRequestContract, CodeExecutionResponseContract
 from .execution_contracts import (
     NodeExecutionContextContract,
@@ -96,6 +106,14 @@ __all__ = [
     "AgentToolRefContract",
     "AgentVersionContract",
     "AgentVersionStatus",
+    "PermissionCheckContract",
+    "PermissionCheckResultContract",
+    "RoleAssignmentContract",
+    "RoleAssignmentStatus",
+    "RoleContract",
+    "RoleStatus",
+    "UserContract",
+    "UserStatus",
     "CodeExecutionRequestContract",
     "CodeExecutionResponseContract",
     "ChatCompletionRequestContract",

+ 60 - 0
libs/core-domain/src/core_domain/auth_contracts.py

@@ -0,0 +1,60 @@
+from datetime import datetime
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+from core_shared import JSONValue
+
+
+UserStatus = Literal["active", "disabled", "deleted"]
+RoleStatus = Literal["active", "disabled"]
+RoleAssignmentStatus = Literal["active", "revoked"]
+
+
+class UserContract(BaseModel):
+    id: str
+    tenant_id: str
+    username: str
+    display_name: str | None = None
+    email: str | None = None
+    status: UserStatus
+    metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
+    last_login_time: datetime | None = None
+    created_time: datetime
+
+
+class RoleContract(BaseModel):
+    id: str
+    tenant_id: str
+    code: str
+    name: str
+    description: str | None = None
+    status: RoleStatus
+    permissions_json: list[str] = Field(default_factory=list)
+    created_time: datetime
+
+
+class RoleAssignmentContract(BaseModel):
+    id: str
+    tenant_id: str
+    user_id: str
+    role_id: str
+    status: RoleAssignmentStatus
+    scope_type: str | None = None
+    scope_id: str | None = None
+    expires_time: datetime | None = None
+    created_time: datetime
+
+
+class PermissionCheckContract(BaseModel):
+    tenant_id: str
+    user_id: str
+    permission: str
+    scope_type: str | None = None
+    scope_id: str | None = None
+
+
+class PermissionCheckResultContract(BaseModel):
+    allowed: bool
+    reason: str
+    matched_role_ids: list[str] = Field(default_factory=list)

+ 27 - 0
services/api-gateway/app/api/routes.py

@@ -193,6 +193,12 @@ def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName,
             path_prefix="/events",
             health_path="/events/health",
         ),
+        "auth-service": ProxyTarget(
+            service_name="auth-service",
+            base_url=settings.auth_service_url,
+            path_prefix="/auth",
+            health_path="/auth/health",
+        ),
     }
 
 
@@ -422,6 +428,27 @@ async def proxy_event_service(
     )
 
 
+@router.api_route(
+    "/gateway/auth",
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
+)
+@router.api_route(
+    "/gateway/auth/{path:path}",
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
+)
+async def proxy_auth_service(
+    request: Request,
+    path: str = "",
+    settings: ApiGatewaySettings = Depends(get_gateway_settings),
+    proxy: ServiceProxy = Depends(get_service_proxy),
+) -> Response:
+    return await proxy.forward(
+        request=request,
+        target=build_proxy_targets(settings)["auth-service"],
+        path=path,
+    )
+
+
 @router.api_route(
     "/gateway/tools",
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"],

+ 1 - 0
services/api-gateway/app/bootstrap/settings.py

@@ -18,6 +18,7 @@ class ApiGatewaySettings(ServiceSettings):
     human_service_url: str = "http://127.0.0.1:8011"
     knowledge_service_url: str = "http://127.0.0.1:8012"
     event_service_url: str = "http://127.0.0.1:8013"
+    auth_service_url: str = "http://127.0.0.1:8014"
     proxy_timeout_seconds: float = 30.0
     downstream_health_timeout_seconds: float = 2.0
     auth_required: bool = False

+ 1 - 0
services/api-gateway/app/infrastructure/proxy.py

@@ -26,6 +26,7 @@ ProxyServiceName = Literal[
     "human-service",
     "knowledge-service",
     "event-service",
+    "auth-service",
 ]
 
 

+ 37 - 0
services/auth-service/alembic.ini

@@ -0,0 +1,37 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = sqlite:///./auth_service.db
+
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

+ 38 - 0
services/auth-service/alembic/env.py

@@ -0,0 +1,38 @@
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+
+from app.db.models import Base
+
+config = context.config
+
+if config.config_file_name is not None:
+    fileConfig(config.config_file_name)
+
+target_metadata = Base.metadata
+
+
+def run_migrations_offline() -> None:
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online() -> None:
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section, {}),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+    with connectable.connect() as connection:
+        context.configure(connection=connection, target_metadata=target_metadata)
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 1 - 0
services/auth-service/alembic/versions/.gitkeep

@@ -0,0 +1 @@
+

+ 94 - 0
services/auth-service/alembic/versions/20260425_0001_init_auth_models.py

@@ -0,0 +1,94 @@
+"""init auth models
+
+Revision ID: 20260425_0001
+Revises:
+Create Date: 2026-04-25 23:55:00
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision: str = "20260425_0001"
+down_revision: str | None = None
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "auth_user",
+        sa.Column("username", sa.String(length=128), nullable=False),
+        sa.Column("display_name", sa.String(length=128), nullable=True),
+        sa.Column("email", sa.String(length=256), nullable=True),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("metadata_json", sa.JSON(), nullable=False),
+        sa.Column("last_login_time", sa.DateTime(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "auth_role",
+        sa.Column("code", sa.String(length=128), nullable=False),
+        sa.Column("name", sa.String(length=128), nullable=False),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("permissions_json", sa.JSON(), nullable=False),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "auth_role_assignment",
+        sa.Column("user_id", sa.String(length=36), nullable=False),
+        sa.Column("role_id", sa.String(length=36), nullable=False),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("scope_type", sa.String(length=64), nullable=True),
+        sa.Column("scope_id", sa.String(length=64), nullable=True),
+        sa.Column("expires_time", sa.DateTime(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    for table_name in ("auth_user", "auth_role", "auth_role_assignment"):
+        op.create_index(f"ix_{table_name}_tenant_id", table_name, ["tenant_id"])
+        op.create_index(f"ix_{table_name}_status", table_name, ["status"])
+    op.create_index("ix_auth_user_username", "auth_user", ["username"])
+    op.create_index("ix_auth_user_email", "auth_user", ["email"])
+    op.create_index("ix_auth_role_code", "auth_role", ["code"])
+    op.create_index("ix_auth_role_assignment_user_id", "auth_role_assignment", ["user_id"])
+    op.create_index("ix_auth_role_assignment_role_id", "auth_role_assignment", ["role_id"])
+    op.create_index("ix_auth_role_assignment_scope_type", "auth_role_assignment", ["scope_type"])
+    op.create_index("ix_auth_role_assignment_scope_id", "auth_role_assignment", ["scope_id"])
+    op.create_index(
+        "ix_auth_role_assignment_expires_time",
+        "auth_role_assignment",
+        ["expires_time"],
+    )
+
+
+def downgrade() -> None:
+    op.drop_table("auth_role_assignment")
+    op.drop_table("auth_role")
+    op.drop_table("auth_user")

+ 1 - 0
services/auth-service/app/__init__.py

@@ -0,0 +1 @@
+

+ 1 - 0
services/auth-service/app/api/__init__.py

@@ -0,0 +1 @@
+

+ 134 - 0
services/auth-service/app/api/routes.py

@@ -0,0 +1,134 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from core_domain import ServiceHealth
+
+from app.application.services import AuthApplicationService
+from app.db.session import get_db
+from app.domain.repositories import RoleAssignmentRepository, RoleRepository, UserRepository
+from app.schemas.auth import (
+    PermissionCheckRequest,
+    PermissionCheckResponse,
+    RoleAssignmentCreateRequest,
+    RoleAssignmentResponse,
+    RoleAssignmentStatusUpdateRequest,
+    RoleCreateRequest,
+    RoleResponse,
+    RoleStatusUpdateRequest,
+    UserCreateRequest,
+    UserResponse,
+    UserStatusUpdateRequest,
+)
+
+router = APIRouter()
+
+
+def get_auth_application_service(db: Session = Depends(get_db)) -> AuthApplicationService:
+    return AuthApplicationService(
+        user_repository=UserRepository(db),
+        role_repository=RoleRepository(db),
+        assignment_repository=RoleAssignmentRepository(db),
+    )
+
+
+@router.get("/health", response_model=ServiceHealth)
+def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="auth-service", status="ok", database="ok")
+
+
+@router.post("/users", response_model=UserResponse)
+def create_user(
+    payload: UserCreateRequest,
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> UserResponse:
+    return UserResponse.from_entity(service.create_user(payload))
+
+
+@router.get("/users", response_model=list[UserResponse])
+def list_users(
+    tenant_id: str = Query(...),
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> list[UserResponse]:
+    return [UserResponse.from_entity(item) for item in service.list_users(tenant_id=tenant_id)]
+
+
+@router.patch("/users/{user_id}/status", response_model=UserResponse)
+def update_user_status(
+    user_id: str,
+    payload: UserStatusUpdateRequest,
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> UserResponse:
+    entity = service.update_user_status(user_id=user_id, payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"user not found: {user_id}")
+    return UserResponse.from_entity(entity)
+
+
+@router.post("/roles", response_model=RoleResponse)
+def create_role(
+    payload: RoleCreateRequest,
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> RoleResponse:
+    return RoleResponse.from_entity(service.create_role(payload))
+
+
+@router.get("/roles", response_model=list[RoleResponse])
+def list_roles(
+    tenant_id: str = Query(...),
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> list[RoleResponse]:
+    return [RoleResponse.from_entity(item) for item in service.list_roles(tenant_id=tenant_id)]
+
+
+@router.patch("/roles/{role_id}/status", response_model=RoleResponse)
+def update_role_status(
+    role_id: str,
+    payload: RoleStatusUpdateRequest,
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> RoleResponse:
+    entity = service.update_role_status(role_id=role_id, payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"role not found: {role_id}")
+    return RoleResponse.from_entity(entity)
+
+
+@router.post("/assignments", response_model=RoleAssignmentResponse)
+def create_assignment(
+    payload: RoleAssignmentCreateRequest,
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> RoleAssignmentResponse:
+    return RoleAssignmentResponse.from_entity(service.create_assignment(payload))
+
+
+@router.get("/assignments", response_model=list[RoleAssignmentResponse])
+def list_assignments(
+    tenant_id: str = Query(...),
+    user_id: str = Query(...),
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> list[RoleAssignmentResponse]:
+    return [
+        RoleAssignmentResponse.from_entity(item)
+        for item in service.list_assignments(tenant_id=tenant_id, user_id=user_id)
+    ]
+
+
+@router.patch("/assignments/{assignment_id}/status", response_model=RoleAssignmentResponse)
+def update_assignment_status(
+    assignment_id: str,
+    payload: RoleAssignmentStatusUpdateRequest,
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> RoleAssignmentResponse:
+    entity = service.update_assignment_status(assignment_id=assignment_id, payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"assignment not found: {assignment_id}")
+    return RoleAssignmentResponse.from_entity(entity)
+
+
+@router.post("/permissions/check", response_model=PermissionCheckResponse)
+def check_permission(
+    payload: PermissionCheckRequest,
+    service: AuthApplicationService = Depends(get_auth_application_service),
+) -> PermissionCheckResponse:
+    return service.check_permission(payload)

+ 1 - 0
services/auth-service/app/application/__init__.py

@@ -0,0 +1 @@
+

+ 147 - 0
services/auth-service/app/application/services.py

@@ -0,0 +1,147 @@
+from datetime import datetime
+
+from app.db.models import Role, RoleAssignment, User
+from app.domain.repositories import RoleAssignmentRepository, RoleRepository, UserRepository
+from app.schemas.auth import (
+    PermissionCheckRequest,
+    PermissionCheckResponse,
+    RoleAssignmentCreateRequest,
+    RoleAssignmentStatusUpdateRequest,
+    RoleCreateRequest,
+    RoleStatusUpdateRequest,
+    UserCreateRequest,
+    UserStatusUpdateRequest,
+)
+
+
+class AuthApplicationService:
+    def __init__(
+        self,
+        *,
+        user_repository: UserRepository,
+        role_repository: RoleRepository,
+        assignment_repository: RoleAssignmentRepository,
+    ) -> None:
+        self.user_repository = user_repository
+        self.role_repository = role_repository
+        self.assignment_repository = assignment_repository
+
+    def create_user(self, payload: UserCreateRequest) -> User:
+        return self.user_repository.create(
+            tenant_id=payload.tenant_id,
+            username=payload.username,
+            display_name=payload.display_name,
+            email=payload.email,
+            metadata_json=payload.metadata_json,
+        )
+
+    def list_users(self, *, tenant_id: str) -> list[User]:
+        return self.user_repository.list_by_tenant(tenant_id=tenant_id)
+
+    def update_user_status(self, *, user_id: str, payload: UserStatusUpdateRequest) -> User | None:
+        return self.user_repository.update_status(
+            tenant_id=payload.tenant_id,
+            user_id=user_id,
+            status=payload.status,
+        )
+
+    def create_role(self, payload: RoleCreateRequest) -> Role:
+        return self.role_repository.create(
+            tenant_id=payload.tenant_id,
+            code=payload.code,
+            name=payload.name,
+            description=payload.description,
+            permissions_json=payload.permissions_json,
+        )
+
+    def list_roles(self, *, tenant_id: str) -> list[Role]:
+        return self.role_repository.list_by_tenant(tenant_id=tenant_id)
+
+    def update_role_status(self, *, role_id: str, payload: RoleStatusUpdateRequest) -> Role | None:
+        return self.role_repository.update_status(
+            tenant_id=payload.tenant_id,
+            role_id=role_id,
+            status=payload.status,
+        )
+
+    def create_assignment(
+        self,
+        payload: RoleAssignmentCreateRequest,
+    ) -> RoleAssignment:
+        return self.assignment_repository.create(
+            tenant_id=payload.tenant_id,
+            user_id=payload.user_id,
+            role_id=payload.role_id,
+            scope_type=payload.scope_type,
+            scope_id=payload.scope_id,
+            expires_time=payload.expires_time,
+        )
+
+    def list_assignments(self, *, tenant_id: str, user_id: str) -> list[RoleAssignment]:
+        return self.assignment_repository.list_by_user(tenant_id=tenant_id, user_id=user_id)
+
+    def update_assignment_status(
+        self,
+        *,
+        assignment_id: str,
+        payload: RoleAssignmentStatusUpdateRequest,
+    ) -> RoleAssignment | None:
+        return self.assignment_repository.update_status(
+            tenant_id=payload.tenant_id,
+            assignment_id=assignment_id,
+            status=payload.status,
+        )
+
+    def check_permission(self, payload: PermissionCheckRequest) -> PermissionCheckResponse:
+        user = self.user_repository.get_by_id(
+            tenant_id=payload.tenant_id,
+            user_id=payload.user_id,
+        )
+        if user is None or user.status != "active":
+            return PermissionCheckResponse(allowed=False, reason="user_not_active")
+
+        assignments = self.assignment_repository.list_by_user(
+            tenant_id=payload.tenant_id,
+            user_id=payload.user_id,
+        )
+        matched_role_ids: list[str] = []
+        now = datetime.utcnow()
+        for assignment in assignments:
+            if assignment.status != "active":
+                continue
+            if assignment.expires_time is not None and assignment.expires_time <= now:
+                continue
+            if not self._scope_matches(
+                assignment=assignment,
+                scope_type=payload.scope_type,
+                scope_id=payload.scope_id,
+            ):
+                continue
+            role = self.role_repository.get_by_id(
+                tenant_id=payload.tenant_id,
+                role_id=assignment.role_id,
+            )
+            if role is None or role.status != "active":
+                continue
+            if self._permission_matches(role.permissions_json, payload.permission):
+                matched_role_ids.append(role.id)
+
+        return PermissionCheckResponse(
+            allowed=bool(matched_role_ids),
+            reason="matched" if matched_role_ids else "permission_not_found",
+            matched_role_ids=matched_role_ids,
+        )
+
+    def _permission_matches(self, permissions: list[str], requested_permission: str) -> bool:
+        return "*" in permissions or requested_permission in permissions
+
+    def _scope_matches(
+        self,
+        *,
+        assignment: RoleAssignment,
+        scope_type: str | None,
+        scope_id: str | None,
+    ) -> bool:
+        if assignment.scope_type is None and assignment.scope_id is None:
+            return True
+        return assignment.scope_type == scope_type and assignment.scope_id == scope_id

+ 1 - 0
services/auth-service/app/bootstrap/__init__.py

@@ -0,0 +1 @@
+

+ 17 - 0
services/auth-service/app/bootstrap/app.py

@@ -0,0 +1,17 @@
+from fastapi import FastAPI
+
+from app.api.routes import router
+from app.bootstrap.settings import AuthServiceSettings
+from app.db.session import build_session_factory
+
+
+def create_app() -> FastAPI:
+    settings = AuthServiceSettings()
+    app = FastAPI(
+        title="agent-platform auth-service",
+        version="0.1.0",
+    )
+    app.state.settings = settings
+    app.state.session_factory = build_session_factory(settings)
+    app.include_router(router, prefix="/auth", tags=["auth"])
+    return app

+ 7 - 0
services/auth-service/app/bootstrap/settings.py

@@ -0,0 +1,7 @@
+from core_shared import ServiceSettings
+
+
+class AuthServiceSettings(ServiceSettings):
+    service_name: str = "auth-service"
+    service_port: int = 8014
+    database_url: str = "sqlite:///./auth_service.db"

+ 1 - 0
services/auth-service/app/db/__init__.py

@@ -0,0 +1 @@
+

+ 7 - 0
services/auth-service/app/db/models/__init__.py

@@ -0,0 +1,7 @@
+from core_db import Base
+
+from .role import Role
+from .role_assignment import RoleAssignment
+from .user import User
+
+__all__ = ["Base", "Role", "RoleAssignment", "User"]

+ 15 - 0
services/auth-service/app/db/models/role.py

@@ -0,0 +1,15 @@
+from sqlalchemy import String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+
+
+class Role(Base, TenantMixin, AuditMixin, VersionMixin):
+    __tablename__ = "auth_role"
+
+    code: Mapped[str] = mapped_column(String(128), index=True)
+    name: Mapped[str] = mapped_column(String(128))
+    description: Mapped[str | None] = mapped_column(Text, nullable=True)
+    status: Mapped[str] = mapped_column(String(32), default="active", index=True)
+    permissions_json: Mapped[list[str]] = mapped_column(JSON, default=list)

+ 17 - 0
services/auth-service/app/db/models/role_assignment.py

@@ -0,0 +1,17 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, String
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+
+
+class RoleAssignment(Base, TenantMixin, AuditMixin, VersionMixin):
+    __tablename__ = "auth_role_assignment"
+
+    user_id: Mapped[str] = mapped_column(String(36), index=True)
+    role_id: Mapped[str] = mapped_column(String(36), index=True)
+    status: Mapped[str] = mapped_column(String(32), default="active", index=True)
+    scope_type: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
+    scope_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
+    expires_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)

+ 19 - 0
services/auth-service/app/db/models/user.py

@@ -0,0 +1,19 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, String
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_shared import JSONValue
+
+
+class User(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "auth_user"
+
+    username: Mapped[str] = mapped_column(String(128), index=True)
+    display_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
+    email: Mapped[str | None] = mapped_column(String(256), nullable=True, index=True)
+    status: Mapped[str] = mapped_column(String(32), default="active", index=True)
+    metadata_json: Mapped[dict[str, JSONValue]] = mapped_column(JSON, default=dict)
+    last_login_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

+ 28 - 0
services/auth-service/app/db/session.py

@@ -0,0 +1,28 @@
+from collections.abc import Generator
+
+from fastapi import Request
+from sqlalchemy.orm import Session, sessionmaker
+
+from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
+
+from app.bootstrap.settings import AuthServiceSettings
+
+
+def build_session_factory(settings: AuthServiceSettings | None = None) -> sessionmaker[Session]:
+    resolved_settings = settings or AuthServiceSettings()
+    engine = create_engine_from_settings(
+        DatabaseSettings(
+            database_url=resolved_settings.database_url,
+            echo_sql=resolved_settings.echo_sql,
+        )
+    )
+    return create_session_factory(engine)
+
+
+def get_db(request: Request) -> Generator[Session, None, None]:
+    session_factory: sessionmaker[Session] = request.app.state.session_factory
+    session = session_factory()
+    try:
+        yield session
+    finally:
+        session.close()

+ 1 - 0
services/auth-service/app/domain/__init__.py

@@ -0,0 +1 @@
+

+ 160 - 0
services/auth-service/app/domain/repositories.py

@@ -0,0 +1,160 @@
+from datetime import datetime
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from core_domain import RoleAssignmentStatus, RoleStatus, UserStatus
+from core_shared import JSONValue
+
+from app.db.models import Role, RoleAssignment, User
+
+
+class UserRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        username: str,
+        display_name: str | None,
+        email: str | None,
+        metadata_json: dict[str, JSONValue],
+    ) -> User:
+        entity = User(
+            tenant_id=tenant_id,
+            username=username,
+            display_name=display_name,
+            email=email,
+            metadata_json=metadata_json,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_tenant(self, *, tenant_id: str) -> list[User]:
+        stmt = select(User).where(User.tenant_id == tenant_id).order_by(User.created_time.desc())
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, user_id: str) -> User | None:
+        stmt = select(User).where(User.tenant_id == tenant_id).where(User.id == user_id)
+        return self.db.scalar(stmt)
+
+    def update_status(self, *, tenant_id: str, user_id: str, status: UserStatus) -> User | None:
+        entity = self.get_by_id(tenant_id=tenant_id, user_id=user_id)
+        if entity is None:
+            return None
+        entity.status = status
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+
+class RoleRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        code: str,
+        name: str,
+        description: str | None,
+        permissions_json: list[str],
+    ) -> Role:
+        entity = Role(
+            tenant_id=tenant_id,
+            code=code,
+            name=name,
+            description=description,
+            permissions_json=permissions_json,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_tenant(self, *, tenant_id: str) -> list[Role]:
+        stmt = select(Role).where(Role.tenant_id == tenant_id).order_by(Role.created_time.desc())
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, role_id: str) -> Role | None:
+        stmt = select(Role).where(Role.tenant_id == tenant_id).where(Role.id == role_id)
+        return self.db.scalar(stmt)
+
+    def update_status(self, *, tenant_id: str, role_id: str, status: RoleStatus) -> Role | None:
+        entity = self.get_by_id(tenant_id=tenant_id, role_id=role_id)
+        if entity is None:
+            return None
+        entity.status = status
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+
+class RoleAssignmentRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        user_id: str,
+        role_id: str,
+        scope_type: str | None,
+        scope_id: str | None,
+        expires_time: datetime | None,
+    ) -> RoleAssignment:
+        entity = RoleAssignment(
+            tenant_id=tenant_id,
+            user_id=user_id,
+            role_id=role_id,
+            scope_type=scope_type,
+            scope_id=scope_id,
+            expires_time=expires_time,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_user(self, *, tenant_id: str, user_id: str) -> list[RoleAssignment]:
+        stmt = (
+            select(RoleAssignment)
+            .where(RoleAssignment.tenant_id == tenant_id)
+            .where(RoleAssignment.user_id == user_id)
+            .order_by(RoleAssignment.created_time.desc())
+        )
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(
+        self,
+        *,
+        tenant_id: str,
+        assignment_id: str,
+    ) -> RoleAssignment | None:
+        stmt = (
+            select(RoleAssignment)
+            .where(RoleAssignment.tenant_id == tenant_id)
+            .where(RoleAssignment.id == assignment_id)
+        )
+        return self.db.scalar(stmt)
+
+    def update_status(
+        self,
+        *,
+        tenant_id: str,
+        assignment_id: str,
+        status: RoleAssignmentStatus,
+    ) -> RoleAssignment | None:
+        entity = self.get_by_id(tenant_id=tenant_id, assignment_id=assignment_id)
+        if entity is None:
+            return None
+        entity.status = status
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity

+ 3 - 0
services/auth-service/app/main.py

@@ -0,0 +1,3 @@
+from app.bootstrap.app import create_app
+
+app = create_app()

+ 1 - 0
services/auth-service/app/schemas/__init__.py

@@ -0,0 +1 @@
+

+ 85 - 0
services/auth-service/app/schemas/auth.py

@@ -0,0 +1,85 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from pydantic import BaseModel, Field
+
+from core_domain import (
+    PermissionCheckContract,
+    PermissionCheckResultContract,
+    RoleAssignmentContract,
+    RoleAssignmentStatus,
+    RoleContract,
+    RoleStatus,
+    UserContract,
+    UserStatus,
+)
+from core_shared import JSONValue
+
+if TYPE_CHECKING:
+    from app.db.models import Role, RoleAssignment, User
+
+
+class UserCreateRequest(BaseModel):
+    tenant_id: str
+    username: str
+    display_name: str | None = None
+    email: str | None = None
+    metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class UserStatusUpdateRequest(BaseModel):
+    tenant_id: str
+    status: UserStatus
+
+
+class UserResponse(UserContract):
+    @classmethod
+    def from_entity(cls, entity: "User") -> "UserResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class RoleCreateRequest(BaseModel):
+    tenant_id: str
+    code: str
+    name: str
+    description: str | None = None
+    permissions_json: list[str] = Field(default_factory=list)
+
+
+class RoleStatusUpdateRequest(BaseModel):
+    tenant_id: str
+    status: RoleStatus
+
+
+class RoleResponse(RoleContract):
+    @classmethod
+    def from_entity(cls, entity: "Role") -> "RoleResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class RoleAssignmentCreateRequest(BaseModel):
+    tenant_id: str
+    user_id: str
+    role_id: str
+    scope_type: str | None = None
+    scope_id: str | None = None
+    expires_time: datetime | None = None
+
+
+class RoleAssignmentStatusUpdateRequest(BaseModel):
+    tenant_id: str
+    status: RoleAssignmentStatus
+
+
+class RoleAssignmentResponse(RoleAssignmentContract):
+    @classmethod
+    def from_entity(cls, entity: "RoleAssignment") -> "RoleAssignmentResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class PermissionCheckRequest(PermissionCheckContract):
+    pass
+
+
+class PermissionCheckResponse(PermissionCheckResultContract):
+    pass

+ 25 - 0
services/auth-service/pyproject.toml

@@ -0,0 +1,25 @@
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "auth-service"
+version = "0.1.0"
+description = "Identity and authorization service for agent platform."
+requires-python = ">=3.11"
+dependencies = [
+  "alembic>=1.13,<2.0",
+  "fastapi>=0.111,<1.0",
+  "uvicorn[standard]>=0.30,<1.0",
+  "pydantic>=2.7,<3.0",
+  "sqlalchemy>=2.0,<3.0",
+  "core-db",
+  "core-domain",
+  "core-shared",
+]
+
+[tool.setuptools]
+package-dir = {"" = "."}
+
+[tool.setuptools.packages.find]
+where = ["."]