feat(auth): 관리자 판별을 Keycloak 역할(role)에서 그룹(group) 기반으로 변경
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 산재 금지).
|
||||
|
||||
15
src/auth.js
15
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,
|
||||
};
|
||||
|
||||
@@ -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 로 메인 언어 자동 판별용 토큰(선택).
|
||||
// 공개 레포는 없어도 되고, 비공개 레포 조회에만 필요.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user