Git 그룹 namespace 동기화
Epic #1081 M2 에서 추가된 Keycloak Group → Gitea Organization 자동 동기화 + 그룹 namespace repo 생성 워크플로우를 정리합니다.
본 문서는 Git Service Operator Guide 의 M2 절을 보완하는 그룹 정책 진실의 원천입니다. 사용자/그룹 repo 분기 정책, namespace 컨벤션, M3 까지의 cutover 로드맵을 한 곳에서 추적합니다.
왜 그룹 namespace 가 필요한가
M1 (PR #1118) 에서 POST /api/v1/git/repos
는 group_id 가 있으면 라우터 안에서 g-<group_id-short> 형태의 즉석
슬러그로 Gitea organization 을 생성했습니다. 이는 세 가지 문제를 만들었습니다:
- 역추적 불가능 —
gitea_org_login→keycloak_group_id매핑이 DB 에 없어 멤버십 reconciliation 이 작동하지 않음. - UI URL 노이즈 —
https://gend.genon.ai/git/g-1a2b3c4d/foo형태로 사람이 읽기 어려움. 그룹명 변경시 슬러그가 정렬되지 않음. - 멤버십 cache 부재 — admin 이 "이 그룹의 Gitea 권한 누가 갖고 있나" 를 추적할 단일 테이블이 없음.
M2 는 이를 gitea_org_sync 테이블 + 3 endpoint 로 정리합니다.
데이터 모델
-
gitea_org_sync는 M2 신규 테이블,gitea_repo_binding은 M1 기존 테이블을 M2 에서 활성화한 것이다. -
gitea_repo_binding의owner_id/group_id는 XOR check 제약이다 — 둘 중 정확히 하나만 채워진다. -
gitea_org_sync.keycloak_group_idUNIQUE → 한 Keycloak 그룹당 한 Gitea org. -
gitea_org_sync.gitea_org_idUNIQUE → 한 Gitea org 당 한 Keycloak 그룹 (역방향도 1:1, polymorphic 매핑 회피). -
gitea_org_sync.gitea_org_loginUNIQUE → Gitea side 의 슬러그도 구조적 유일성. DB 레벨 충돌은 IntegrityError → race 분기에서 winner 행 재읽기 (GitProvisionService.provision_user와 동일 패턴).
API 표면
3 endpoint 가 추가됩니다. 전수 시그니처는 Git Service Operator Guide §M2 참조.
POST /api/v1/git/orgs/sync (admin)
Keycloak 그룹 1건을 Gitea organization 으로 idempotent sync 합니다.
curl -fsS -X POST "$API/api/v1/git/orgs/sync" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"keycloak_group_id": "11111111-2222-3333-4444-555555555555",
"keycloak_group_name": "Team Alpha"
}'
- 첫 호출: Gitea
GET /api/v1/orgs/team-alpha(404) →POST /api/v1/orgs(created) →INSERT gitea_org_sync. - 두 번째 호출: fast-path 로 동일 행 반환 (Gitea 미호출).
- Gitea 에 이미 org 가 존재하는 경우 (operator 가 수동 생성): adopt 모드
로
INSERT gitea_org_sync(gitea_org_id는 Gitea 응답에서 가져옴).
POST /api/v1/git/orgs/{org_login}/repos (member or admin)
그룹 namespace 아래 repo 생성. 사용자/그룹 분기를 라우터 안에 흩어두지 않고 별도 endpoint 로 분리해 ACL 점검을 단순화합니다.
# 그룹 멤버 토큰 — token claim 의 groups 에 keycloak_group_id 가 있어야 통과
curl -fsS -X POST "$API/api/v1/git/orgs/team-alpha/repos" \
-H "Authorization: Bearer $MEMBER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name": "notebooks", "description": "Team Alpha shared notebooks"}'
응답: GiteaRepoResponse (full_name="team-alpha/notebooks", visibility="group",
owner_id=null, group_id=<keycloak_group_id>, clone_url=...).
404 조건: gitea_org_sync 에 binding 없음 → admin 이 먼저 /orgs/sync 호출 필요.
403 조건: non-admin 사용자의 token claim groups 에 keycloak_group_id
없음.
POST /api/v1/git/orgs/membership/sync (admin)
사용자별 Gitea team 멤버십을 Keycloak 그룹 리스트와 reconcile.
curl -fsS -X POST "$API/api/v1/git/orgs/membership/sync" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"keycloak_sub": "user-sub-uuid",
"group_names": ["Team Alpha", "Team Beta"]
}'
응답:
{ "added": ["team-alpha"], "skipped": ["Team Beta"] }
added: 멤버십 PUT 이 200/204/422-already 로 처리된 Gitea org 슬러그.skipped:gitea_org_syncbinding 없는 Keycloak 그룹명 — admin 이/orgs/sync먼저 호출 필요.
M2 는 additive only. Keycloak 그룹에서 제거된 멤버를 Gitea 에서도
revoke 하는 경로는 M3 입니다 (Gitea "내 org 목록" API wrapper 필요).
이 한계 때문에 본 endpoint 의 응답은 removed: [] 필드를 노출하지
않습니다 — API contract 가 M3 에서 확장될 때 silent breaking 을 피하기
위해서.
슬러그 컨벤션
gend_api.services.git.group_sync._slugify_org_login 가 단일 진실의 원천:
| 입력 | 출력 |
|---|---|
Team Alpha | team-alpha |
Data Science Group | data-science-group |
research-ml | research-ml |
한글 그룹 | group-<hex8> (한글은 ASCII 대체 후 빈 슬러그 → fallback) |
| (200 자) | 첫 40 자만 (Gitea org login max) |
규칙:
- 소문자화 →
[a-z0-9._-]외 문자는-로 치환 → 양끝-._트림. - 빈 슬러그는
group-<token_hex(4)>로 fallback (동일 그룹은 매 호출마다 새 슬러그가 나오므로 의도적으로 호출 1회만 발생하도록 idempotent fast-path 가 보호). - Gitea org login max 40 char. 더 긴 입력은 40 자에서 잘림.
Audit event
gend_api.services.git.git_audit.GIT_AUDIT_EVENTS 에 3개 신규 이벤트가 추가됩니다:
| Event | Actor | 설명 |
|---|---|---|
git.org.sync | keycloak_group_id | admin 이 /orgs/sync 로 새 org binding 생성 (fast-path 호출은 audit 미발생). |
git.org.membership | keycloak_sub | 멤버십 reconcile 가 add/skip 1건 이상 처리 (no-op 호출은 audit 미발생). |
git.org.repo.create | actor_id | 그룹 namespace 아래 repo 신규 생성 (full_name + org_login + group_id 기록). |
OpenSearch gend-audit-* 인덱스에서 그룹별 활동을 추적할 때 위 3 이벤트로
필터하면 됩니다 (category=git AND event=git.org.*).
M3 로드맵
| 항목 | 트리거 | 비고 |
|---|---|---|
| Keycloak event listener | 그룹 create / 멤버십 변경 → /orgs/sync + /membership/sync 자동 호출 | keycloak.events.AdminEventListenerProvider 구현 + Helm Chart 배포 |
| Membership removal | Keycloak 그룹에서 제거된 멤버 → Gitea team revoke | GiteaClient.list_user_orgs wrap + 차집합 계산 |
| Pre-receive hook | force-push 차단 + repo size 제한 | Gitea app.ini 의 [git.config] + 별도 hook script |
| Hybrid profile | 사용자 + 그룹 dual namespace 노출 (UI 트리) | UI sidebar 그룹 트리 + /api/v1/git/orgs/{org}/repos GET endpoint |