From b4c9b4a3956f19a6c92a29bcd6b2746a5cb969f2 Mon Sep 17 00:00:00 2001 From: 2620227 Date: Thu, 2 Jul 2026 14:47:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8C=90=EB=B3=84=EC=9D=84=20Keycloak=20=EC=97=AD=ED=95=A0(rol?= =?UTF-8?q?e)=EC=97=90=EC=84=9C=20=EA=B7=B8=EB=A3=B9(group)=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth.js: realm_access.roles/resource_access 대신 ID 토큰 claims.groups 사용 (group-membership 매퍼, full.path=false 대비 앞 슬래시 제거 후 비교) - config.js: adminRole(ADMIN_ROLE, "admin") → adminGroup(ADMIN_GROUP, "DevOps") - server.js: 세션 user 주석 roles → groups - CLAUDE.md: 인증 모델·Kubero Env 문서 갱신 Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 7 ++++--- src/auth.js | 15 +++++---------- src/config.js | 5 +++-- src/server.js | 2 +- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c63dd0f..c731e32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,8 +45,9 @@ AI DEV — 사내 개발 포털. 직원이 한 곳에서 **사내 개발 사이 - `.claude/` — Claude Code 훅·설정 ## 인증 모델 -- **사번 = Keycloak `preferred_username`.** 세션에 `req.session.user = { sabun, name, email, roles, isAdmin }`. -- **관리자 = `realm_access.roles` 에 `ADMIN_ROLE`(기본 `admin`) 포함.** 인프라 도구(OpenEverest/MinIO/ +- **사번 = Keycloak `preferred_username`.** 세션에 `req.session.user = { sabun, name, email, groups, isAdmin }`. +- **관리자 = `groups` 클레임에 `ADMIN_GROUP`(기본 `DevOps`) 포함.** (portal 클라이언트의 group-membership + 매퍼가 ID 토큰에 `groups` 를 넣어줌. `full.path=false` 라 앞 슬래시는 떼고 비교.) 인프라 도구(OpenEverest/MinIO/ Keycloak/LiteLLM)는 관리자에게만 렌더(`sites.js` 의 `adminOnly` + `toolsForUser`). - 미인증으로 보호 페이지 진입 시 중간 화면 없이 곧장 Keycloak 로그인으로 리다이렉트 (`requireAuth` → `loginRedirect`). OIDC discovery 실패 시에만 `login.ejs` 폴백. @@ -59,7 +60,7 @@ AI DEV — 사내 개발 포털. 직원이 한 곳에서 **사내 개발 사이 - `Dockerfile` 은 Kubero buildpack 의 harbor push 실패를 우회하는 **수동 push 용**. - 도메인 `portal.bokdev.in` (`deploy/portal-route.yaml`). 도메인 추가 시 Keycloak `portal` client redirect 에 `https://<도메인>/auth/callback` 등록 필요. -- Kubero Env: `BASE_URL`, `DATABASE_URL`, `OIDC_*`, `SESSION_SECRET`, `PORTAL_BRAND`, `ADMIN_ROLE`, (선택) 도구 `*_URL`. +- Kubero Env: `BASE_URL`, `DATABASE_URL`, `OIDC_*`, `SESSION_SECRET`, `PORTAL_BRAND`, `ADMIN_GROUP`, (선택) 도구 `*_URL`. ## 컨벤션 - 라우트는 `src/server.js` 에, DB 접근은 `src/db.js` 의 함수로만 (인라인 SQL 산재 금지). diff --git a/src/auth.js b/src/auth.js index 0a643df..ce24d72 100644 --- a/src/auth.js +++ b/src/auth.js @@ -42,20 +42,15 @@ export async function handleCallback(req) { nonce, }); const claims = tokenSet.claims(); - // Keycloak realm 역할: realm_access.roles. (클라이언트 역할은 resource_access[client].roles) - const realmRoles = (claims.realm_access && claims.realm_access.roles) || []; - const clientRoles = - (claims.resource_access && - claims.resource_access[config.oidc.clientId] && - claims.resource_access[config.oidc.clientId].roles) || - []; - const roles = [...new Set([...realmRoles, ...clientRoles])]; + // 그룹 기반 관리자 판별. Keycloak group-membership 매퍼가 ID 토큰에 groups 를 넣는다. + // full.path=false 라 보통 "DevOps" 형태지만, 설정에 따라 "/DevOps" 일 수 있어 앞 슬래시는 떼고 비교. + const groups = (Array.isArray(claims.groups) ? claims.groups : []).map((g) => g.replace(/^\//, "")); return { sabun: claims.preferred_username, // 사번 name: claims.name || claims.given_name || claims.preferred_username, email: claims.email, - roles, - isAdmin: roles.includes(config.adminRole), + groups, + isAdmin: groups.includes(config.adminGroup), // SSO 로그아웃(end-session) 시 id_token_hint 로 사용 id_token: tokenSet.id_token, }; diff --git a/src/config.js b/src/config.js index a3ad670..91c24e1 100644 --- a/src/config.js +++ b/src/config.js @@ -13,8 +13,9 @@ export const config = { // redirect 는 baseUrl + /auth/callback 로 구성 }, - // 관리자 판별용 Keycloak 역할 이름. 이 role 을 가진 사용자에게만 인프라 도구 노출. - adminRole: process.env.ADMIN_ROLE || "admin", + // 관리자 판별용 Keycloak 그룹 이름. 이 그룹에 속한 사용자에게만 인프라 도구 노출. + // (groups 클레임은 portal 클라이언트의 group-membership 매퍼가 ID 토큰에 넣어줌) + adminGroup: process.env.ADMIN_GROUP || "DevOps", // 프로젝트 등록 시 Gitea 언어 통계 API 로 메인 언어 자동 판별용 토큰(선택). // 공개 레포는 없어도 되고, 비공개 레포 조회에만 필요. diff --git a/src/server.js b/src/server.js index e2f1424..bb7b7ba 100644 --- a/src/server.js +++ b/src/server.js @@ -65,7 +65,7 @@ app.get("/auth/callback", async (req, res) => { const { id_token, ...user } = u; req.session.idToken = id_token; await db.upsertUser(user); - req.session.user = user; // { sabun, name, email, roles, isAdmin } + req.session.user = user; // { sabun, name, email, groups, isAdmin } res.redirect("/"); } catch (e) { console.error("oidc callback error", e);