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:
2026-07-02 14:47:34 +09:00
parent ac545651e6
commit b4c9b4a395
4 changed files with 13 additions and 16 deletions

View File

@@ -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,
};

View File

@@ -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 로 메인 언어 자동 판별용 토큰(선택).
// 공개 레포는 없어도 되고, 비공개 레포 조회에만 필요.

View File

@@ -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);