diff --git a/backend/.env.development b/backend/.env.development index 0e2afa4..cd341cc 100644 --- a/backend/.env.development +++ b/backend/.env.development @@ -21,6 +21,4 @@ AUTHENTIK_USERINFO_ENDPOINT=https://auth.ose.tw/application/o/userinfo/ PUBLIC_FRONTEND_ORIGINS=http://127.0.0.1:5173,http://localhost:5173 INTERNAL_SHARED_SECRET=CHANGE_ME -ADMIN_ALLOWLIST_EMAILS=chris@ose.tw -ADMIN_ALLOWLIST_SUBS=17a35b0a03a752d60617cf2de2bef2aaf0f0f0f53f24e5bf33c3e7abb6c06e87 -ADMIN_REQUIRED_GROUPS= +ADMIN_REQUIRED_GROUPS=member-admin diff --git a/backend/.env.example b/backend/.env.example index e28c7a4..8ff5dc4 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -21,6 +21,4 @@ AUTHENTIK_USERINFO_ENDPOINT= PUBLIC_FRONTEND_ORIGINS=https://member.ose.tw,https://mkt.ose.tw,https://admin.ose.tw INTERNAL_SHARED_SECRET=CHANGE_ME -ADMIN_ALLOWLIST_EMAILS= -ADMIN_ALLOWLIST_SUBS= -ADMIN_REQUIRED_GROUPS= +ADMIN_REQUIRED_GROUPS=member-admin diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index fa9115f..5e5b81e 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.db.session import get_db -from app.models.api_client import ApiClient from app.repositories.companies_repo import CompaniesRepository from app.repositories.modules_repo import ModulesRepository from app.repositories.permissions_repo import PermissionsRepository @@ -17,7 +16,6 @@ from app.schemas.permissions import ( PermissionGrantRequest, PermissionRevokeRequest, ) -from app.security.api_client_auth import require_api_client from app.security.admin_guard import require_admin_principal router = APIRouter( @@ -67,7 +65,6 @@ def _resolve_scope_ids(db: Session, scope_type: str, scope_id: str) -> tuple[str @router.post("/permissions/grant") def grant_permission( payload: PermissionGrantRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, str]: users_repo = UsersRepository(db) @@ -96,7 +93,6 @@ def grant_permission( @router.post("/permissions/revoke") def revoke_permission( payload: PermissionRevokeRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, int | str]: users_repo = UsersRepository(db) @@ -121,7 +117,6 @@ def revoke_permission( @router.get("/permissions/direct", response_model=DirectPermissionListResponse) def list_direct_permissions( - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), keyword: str | None = Query(default=None), scope_type: str | None = Query(default=None), @@ -146,7 +141,6 @@ def list_direct_permissions( @router.delete("/permissions/direct/{permission_id}") def delete_direct_permission( permission_id: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, int | str]: try: diff --git a/backend/app/api/admin_catalog.py b/backend/app/api/admin_catalog.py index 881ac79..3d32ec7 100644 --- a/backend/app/api/admin_catalog.py +++ b/backend/app/api/admin_catalog.py @@ -4,7 +4,6 @@ from sqlalchemy.orm import Session from app.core.keygen import generate_key from app.core.config import get_settings from app.db.session import get_db -from app.models.api_client import ApiClient from app.repositories.companies_repo import CompaniesRepository from app.repositories.modules_repo import ModulesRepository from app.repositories.permission_groups_repo import PermissionGroupsRepository @@ -39,7 +38,6 @@ from app.schemas.catalog import ( SystemUpdateRequest, ) from app.schemas.permissions import PermissionGrantRequest, PermissionRevokeRequest -from app.security.api_client_auth import require_api_client from app.security.admin_guard import require_admin_principal from app.services.authentik_admin_service import AuthentikAdminService @@ -126,7 +124,6 @@ def _sync_member_to_authentik( @router.get("/systems") def list_systems( - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), @@ -139,7 +136,6 @@ def list_systems( @router.post("/systems", response_model=SystemItem) def create_system( payload: SystemCreateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> SystemItem: repo = SystemsRepository(db) @@ -152,7 +148,6 @@ def create_system( def update_system( system_key: str, payload: SystemUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> SystemItem: repo = SystemsRepository(db) @@ -165,7 +160,6 @@ def update_system( @router.get("/modules") def list_modules( - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), limit: int = Query(default=200, ge=1, le=500), offset: int = Query(default=0, ge=0), @@ -191,7 +185,6 @@ def list_modules( @router.post("/modules", response_model=ModuleItem) def create_module( payload: ModuleCreateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> ModuleItem: systems_repo = SystemsRepository(db) @@ -213,7 +206,6 @@ def create_module( def update_module( module_key: str, payload: ModuleUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> ModuleItem: modules_repo = ModulesRepository(db) @@ -227,7 +219,6 @@ def update_module( @router.get("/systems/{system_key}/groups") def list_system_groups( system_key: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, list[dict]]: systems_repo = SystemsRepository(db) @@ -246,7 +237,6 @@ def list_system_groups( @router.get("/systems/{system_key}/members") def list_system_members( system_key: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, list[dict]]: systems_repo = SystemsRepository(db) @@ -270,7 +260,6 @@ def list_system_members( @router.get("/modules/{module_key}/groups") def list_module_groups( module_key: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, list[dict]]: modules_repo = ModulesRepository(db) @@ -290,7 +279,6 @@ def list_module_groups( @router.get("/modules/{module_key}/members") def list_module_members( module_key: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, list[dict]]: modules_repo = ModulesRepository(db) @@ -314,7 +302,6 @@ def list_module_members( @router.get("/companies") def list_companies( - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), keyword: str | None = Query(default=None), limit: int = Query(default=100, ge=1, le=500), @@ -328,7 +315,6 @@ def list_companies( @router.post("/companies", response_model=CompanyItem) def create_company( payload: CompanyCreateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> CompanyItem: repo = CompaniesRepository(db) @@ -341,7 +327,6 @@ def create_company( def update_company( company_key: str, payload: CompanyUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> CompanyItem: repo = CompaniesRepository(db) @@ -355,7 +340,6 @@ def update_company( @router.get("/companies/{company_key}/sites") def list_company_sites( company_key: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, list[dict]]: companies_repo = CompaniesRepository(db) @@ -380,7 +364,6 @@ def list_company_sites( @router.get("/sites") def list_sites( - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), company_key: str | None = Query(default=None), keyword: str | None = Query(default=None), @@ -420,7 +403,6 @@ def list_sites( @router.post("/sites", response_model=SiteItem) def create_site( payload: SiteCreateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> SiteItem: companies_repo = CompaniesRepository(db) @@ -437,7 +419,6 @@ def create_site( def update_site( site_key: str, payload: SiteUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> SiteItem: companies_repo = CompaniesRepository(db) @@ -462,7 +443,6 @@ def update_site( @router.get("/members") def list_members( - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), keyword: str | None = Query(default=None), limit: int = Query(default=100, ge=1, le=500), @@ -476,7 +456,6 @@ def list_members( @router.post("/members/upsert", response_model=MemberItem) def upsert_member( payload: MemberUpsertRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberItem: users_repo = UsersRepository(db) @@ -517,7 +496,6 @@ def upsert_member( def update_member( authentik_sub: str, payload: MemberUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberItem: users_repo = UsersRepository(db) @@ -558,7 +536,6 @@ def update_member( @router.get("/members/{authentik_sub}/permission-groups", response_model=MemberPermissionGroupsResponse) def get_member_permission_groups( authentik_sub: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberPermissionGroupsResponse: users_repo = UsersRepository(db) @@ -574,7 +551,6 @@ def get_member_permission_groups( def set_member_permission_groups( authentik_sub: str, payload: MemberPermissionGroupsUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberPermissionGroupsResponse: users_repo = UsersRepository(db) @@ -596,7 +572,6 @@ def set_member_permission_groups( @router.get("/permission-groups") def list_permission_groups( - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), @@ -609,7 +584,6 @@ def list_permission_groups( @router.get("/permission-groups/{group_key}/permissions") def list_permission_group_permissions( group_key: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, list[dict]]: repo = PermissionGroupsRepository(db) @@ -636,7 +610,6 @@ def list_permission_group_permissions( @router.get("/permission-groups/{group_key}/bindings", response_model=GroupBindingSnapshot) def get_permission_group_bindings( group_key: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> GroupBindingSnapshot: repo = PermissionGroupsRepository(db) @@ -658,7 +631,6 @@ def get_permission_group_bindings( def replace_permission_group_bindings( group_key: str, payload: GroupBindingUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> GroupBindingSnapshot: repo = PermissionGroupsRepository(db) @@ -715,7 +687,6 @@ def replace_permission_group_bindings( @router.post("/permission-groups", response_model=PermissionGroupItem) def create_permission_group( payload: PermissionGroupCreateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> PermissionGroupItem: repo = PermissionGroupsRepository(db) @@ -728,7 +699,6 @@ def create_permission_group( def update_permission_group( group_key: str, payload: PermissionGroupUpdateRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> PermissionGroupItem: repo = PermissionGroupsRepository(db) @@ -743,7 +713,6 @@ def update_permission_group( def add_group_member( group_key: str, authentik_sub: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, str]: groups_repo = PermissionGroupsRepository(db) @@ -758,7 +727,6 @@ def add_group_member( def remove_group_member( group_key: str, authentik_sub: str, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, int | str]: groups_repo = PermissionGroupsRepository(db) @@ -773,7 +741,6 @@ def remove_group_member( def grant_group_permission( group_key: str, payload: PermissionGrantRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, str]: groups_repo = PermissionGroupsRepository(db) @@ -798,7 +765,6 @@ def grant_group_permission( def revoke_group_permission( group_key: str, payload: PermissionRevokeRequest, - _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, int | str]: groups_repo = PermissionGroupsRepository(db) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index cc8964e..64afa21 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -30,8 +30,6 @@ class Settings(BaseSettings): public_frontend_origins: Annotated[list[str], NoDecode] = ["https://member.ose.tw"] internal_shared_secret: str = "" - admin_allowlist_emails: Annotated[list[str], NoDecode] = [] - admin_allowlist_subs: Annotated[list[str], NoDecode] = [] admin_required_groups: Annotated[list[str], NoDecode] = [] @field_validator("public_frontend_origins", mode="before") @@ -43,7 +41,7 @@ class Settings(BaseSettings): return [] return [origin.strip() for origin in value.split(",") if origin.strip()] - @field_validator("admin_allowlist_emails", "admin_allowlist_subs", "admin_required_groups", mode="before") + @field_validator("admin_required_groups", mode="before") @classmethod def parse_csv(cls, value: str | list[str]) -> list[str]: if isinstance(value, list): diff --git a/backend/app/security/admin_guard.py b/backend/app/security/admin_guard.py index 117b936..f669858 100644 --- a/backend/app/security/admin_guard.py +++ b/backend/app/security/admin_guard.py @@ -9,18 +9,14 @@ def require_admin_principal( principal: AuthentikPrincipal = Depends(require_authenticated_principal), ) -> AuthentikPrincipal: settings = get_settings() - allowed_emails = {email.lower() for email in settings.admin_allowlist_emails} - allowed_subs = set(settings.admin_allowlist_subs) required_groups = {group.lower() for group in settings.admin_required_groups} - if not allowed_emails and not allowed_subs and not required_groups: + if not required_groups: raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="admin_policy_not_configured") - email_ok = bool(principal.email and principal.email.lower() in allowed_emails) - sub_ok = principal.sub in allowed_subs principal_groups = {group.lower() for group in principal.groups} group_ok = bool(required_groups.intersection(principal_groups)) - if not (email_ok or sub_ok or group_ok): + if not group_ok: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="admin_forbidden") return principal diff --git a/frontend/.env.development b/frontend/.env.development index 75d0217..dd13e1d 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,4 +1,2 @@ VITE_APP_TITLE=member.ose.tw (dev) VITE_API_BASE_URL=http://127.0.0.1:8000 -VITE_ADMIN_CLIENT_KEY=admin-frontend -VITE_ADMIN_API_KEY=dev-admin-key-123 diff --git a/frontend/.env.example b/frontend/.env.example index e2ae621..2cb3449 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,5 +1,3 @@ # member.ose.tw frontend env VITE_APP_TITLE=member.ose.tw VITE_API_BASE_URL=https://memberapi.ose.tw -VITE_ADMIN_CLIENT_KEY= -VITE_ADMIN_API_KEY= diff --git a/frontend/src/api/http.js b/frontend/src/api/http.js index 64b3fd2..12f5212 100644 --- a/frontend/src/api/http.js +++ b/frontend/src/api/http.js @@ -2,8 +2,6 @@ import axios from 'axios' import router from '@/router' const BASE_URL = import.meta.env.VITE_API_BASE_URL -const ENV_ADMIN_CLIENT_KEY = import.meta.env.VITE_ADMIN_CLIENT_KEY -const ENV_ADMIN_API_KEY = import.meta.env.VITE_ADMIN_API_KEY // 使用者 API:帶 Bearer token export const userHttp = axios.create({ baseURL: BASE_URL }) @@ -27,7 +25,7 @@ userHttp.interceptors.response.use( } ) -// 管理員 API:帶 X-Client-Key / X-API-Key +// 管理員 API:只帶 Bearer token(後端再檢查 admin 群組) export const adminHttp = axios.create({ baseURL: BASE_URL }) adminHttp.interceptors.request.use(config => { @@ -35,16 +33,6 @@ adminHttp.interceptors.request.use(config => { if (token) { config.headers['Authorization'] = `Bearer ${token}` } - const clientKey = sessionStorage.getItem('admin_client_key') || ENV_ADMIN_CLIENT_KEY - const apiKey = sessionStorage.getItem('admin_api_key') || ENV_ADMIN_API_KEY - if (clientKey && !sessionStorage.getItem('admin_client_key')) { - sessionStorage.setItem('admin_client_key', clientKey) - } - if (apiKey && !sessionStorage.getItem('admin_api_key')) { - sessionStorage.setItem('admin_api_key', apiKey) - } - if (clientKey) config.headers['X-Client-Key'] = clientKey - if (apiKey) config.headers['X-API-Key'] = apiKey return config }) diff --git a/frontend/src/components/AdminCredsCard.vue b/frontend/src/components/AdminCredsCard.vue deleted file mode 100644 index 383e6bd..0000000 --- a/frontend/src/components/AdminCredsCard.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - 管理員認證 - 已儲存(session) - 未設定 - - - - - - - - - - - 儲存認證 - 清除 - - - - - - diff --git a/frontend/src/stores/permission.js b/frontend/src/stores/permission.js index 6debe10..b529e25 100644 --- a/frontend/src/stores/permission.js +++ b/frontend/src/stores/permission.js @@ -5,19 +5,6 @@ import { grantPermission, revokePermission } from '@/api/permission-admin' export const usePermissionStore = defineStore('permission', () => { const snapshot = ref(null) - const envClientKey = import.meta.env.VITE_ADMIN_CLIENT_KEY || '' - const envApiKey = import.meta.env.VITE_ADMIN_API_KEY || '' - const adminClientKey = ref(sessionStorage.getItem('admin_client_key') || envClientKey) - const adminApiKey = ref(sessionStorage.getItem('admin_api_key') || envApiKey) - - if (adminClientKey.value) { - sessionStorage.setItem('admin_client_key', adminClientKey.value) - } - if (adminApiKey.value) { - sessionStorage.setItem('admin_api_key', adminApiKey.value) - } - - const hasAdminCreds = () => !!(adminClientKey.value && adminApiKey.value) async function fetchMySnapshot() { const res = await getMyPermissionSnapshot() @@ -25,20 +12,6 @@ export const usePermissionStore = defineStore('permission', () => { return res.data } - function setAdminCreds(clientKey, apiKey) { - adminClientKey.value = clientKey - adminApiKey.value = apiKey - sessionStorage.setItem('admin_client_key', clientKey) - sessionStorage.setItem('admin_api_key', apiKey) - } - - function clearAdminCreds() { - adminClientKey.value = '' - adminApiKey.value = '' - sessionStorage.removeItem('admin_client_key') - sessionStorage.removeItem('admin_api_key') - } - async function grant(data) { const res = await grantPermission(data) return res.data @@ -51,12 +24,7 @@ export const usePermissionStore = defineStore('permission', () => { return { snapshot, - adminClientKey, - adminApiKey, - hasAdminCreds, fetchMySnapshot, - setAdminCreds, - clearAdminCreds, grant, revoke }