사용자 및 팀
포털은 권한을 하나의 조직, 다수의 팀, 세 가지 역할로 모델링합니다. 모든 사용자는 하나 이상의 팀에 소속되며 프로젝트는 팀에 귀속됩니다. 배포당 정확히 하나의 조직이 존재합니다.
배포를 셋업하는 super-admin; 자기 팀의 멤버십을 관리하는 team admin.
모델
Organization (배포당 하나)
├── Super Admin — 시스템 전반(install.sh 이후의 본인)
├── Team A
│ ├── Team Admin — 팀 설정·멤버 관리
│ └── Developer — 스캔 실행, 결과 분류
└── Team B
└── ...
- Organization — 배포의 경계. super-admin은 조직 단위.
- Team — 프로젝트·스캔·결과가 속하는 단위.
- User — 이메일 + 비밀번호(또는 데모 SaaS의 OAuth ID)를 가진 사람.
역할
| 역할 | 범위 | 권한 |
|---|---|---|
super_admin | 조직 전반 | 모든 admin 화면(/admin/**). 팀 생성·삭제. 모든 프로젝트 편집. 모든 감사 로그 읽기. |
team_admin | 팀별 | 팀 멤버십·설정 관리. 팀 소유 프로젝트 편집. 승인 처리. 팀 API Key 관리. |
developer | 팀별 | 팀 프로 젝트 읽기. 프로젝트 생성·편집. 스캔 실행·취소. 결과 분류(VEX 상태). 멤버·설정 관리는 불가. |
역할은 여러 팀에 걸쳐 누적됩니다 — 사용자는 한 팀에서 team_admin이고 다른 팀에서 developer일 수 있습니다. 역할은 프로젝트 소속 팀 기준으로 평가됩니다.
super_admin은 팀별 역할이 아닙니다 — 팀 멤버십과 무관하게 조직 전반 접근을 부여합니다.
Users 페이지
/admin/users 페이지는 배포 내 모든 계정을 역할 배지, 활성화 상태, 마지막 로그인 시간, 팀 멤버십 카운트와 함께 보여줍니다. 이메일·이름으로 검색하고 역할·상태로 필터링할 수 있습니다.

/admin/teams 페이지는 팀 목록과 각 팀이 보유한 프로젝트·멤버 수를 보여줍니다:

새 사용자 온보딩
v0.10.0 에서는 포털이 초대 이메일을 보내지 않습니다. 새 사용자는 회사 이메일로 /register에서 셀프 가입하며, 비밀번호 정책은 가입 시점에 강제됩니다(12자 이상, bcrypt cost 12, NIST 차단 비밀번호 제외).
가입 후, super_admin이 사용자를 적절한 팀에 추가하고 역할을 배정합니다.
- 사용자에게
/register에서 가입을 요청합니다. - /admin/users에 사용자가 나타나면 사용자 드로어를 엽니다.
- Add to team(또는 팀의 Members → Add member 흐름)으로 선택한 역할의 팀 멤버십을 부여합니다.
동료 온보딩
v0.10.0 에서 포털은 초대 이메일을 보내지 않습니다. 흐름은 다음과 같습니다:
- 관리자가
/admin/teams → New team에서 팀을 만듭니다. (아래 UI 흐름은 team UUID 가 필요하지 않습니다 — 관리자는 이메일로 동료를 식별합니다. UUID 가 필요한 경로는 스크립트 기반 대량 온보딩뿐이며,GET /v1/admin/teams와 본 섹션 하단의 일괄 레시피를 참고하세요.) - 동료가
https://<your-host>/register에서 셀프 가입합니다. - 관리자가
/admin/users → <user>에서 해당 사용자의 행을 열고 드로어 → Memberships → Add to team 으로 팀과 역할을 선택합니다 (기본값으로developer가 안전).
결과: 동료는 다음 로그 인부터 팀의 프로젝트를 볼 수 있습니다.
각 동료가 가입을 마친 뒤에는
POST /v1/admin/teams/{team_id}/members {user_id, role} 로
대량 온보딩을 스크립트화할 수 있습니다.
기존 사용자를 팀에 추가
사용자는 여러 팀에 소속될 수 있습니다. 기존 사용자 추가:
- /admin/teams(super-admin) 또는 Team settings → Members(team admin).
- Add member → 이메일로 검색 → 역할 선택.
사용자가 즉시 추가됩니다; 이메일 확인 단계 없음(이미 계정 보유).
사용자 역할 변경
/admin/users → 사용자 드로어는 Role 드롭다운과 Memberships 섹션을 노출합니다. 한 사용자가 팀마다 다른 역할을 보유할 수 있습니다(팀 A 에서 team_admin, 팀 B 에서 developer) — Memberships 리스트가 모든 배정을 표시하며 인라인으로 편집합니다. Role 드롭다운은 Memberships 리스트에서 선택된 팀의 역할을 설정합니다(또는 super_admin 으로 승격할 때의 글로벌 역할).
- /admin/users → 사용자 → Role.
- 새 역할 선택 → 제출.
감사 로그는 변경을 users 쓰기로 기록하며 역할 diff가 diff 컬럼에 담깁니다(감사 행의 target_table은 users).
팀에서 사용자 제거
- Team settings → Members → 사용자 → Remove.
사용자는 팀의 프로젝트 접근을 잃지만 계정은 남습니다. 계정 자체를 비활성화하려면 비활성화 참고.
마지막 super-admin 보호
포털은 조직의 마지막 활성 super_admin 강등·비활성화를 거부합니다. 사전 체크는 SELECT … FOR UPDATE 트랜잭션 안에서 실행되어 동시 강등 시도가 경합되지 않고 직렬화됩니다. 시도하면 API가 다음을 반환합니다.
{
"type": "about:blank",
"title": "Last Super Admin Protected",
"status": 422,
"detail": "At least one active super_admin must remain in the organization.",
"instance": "/v1/admin/users/01H…/role",
"last_super_admin_protected": true
}
last_super_admin_protected: true 확장 필드는 클라이언트가 본 가드를 일반적인 422 검증 실패와 구분할 수 있게 합니다.
마지막 super-admin 교체:
- 다른 사용자를
super_admin으로 먼저 승격. - 그 다음 원래 사용자를 강등·비활성화.
가드는 두 계층으로 강제됩니다.
- API 계층 —
admin_user_service의SELECT … FOR UPDATE행 락 카운트가 commit 전에 강등·비활성화 시도를 거부. - DB 계층 — PostgreSQL 트리거(
trg_last_super_admin, 마이그레이션0013)가 활성 super-admin 수를 0 으로 만드는 모든UPDATE/DELETE에 대해SQLSTATE 23514를 발생. 직접psql쓰기로 API 를 우회하더라도 동일하게 차단됩니다. 어느 계층이 잡았든 동일한last_super_admin_protectedProblem Details 확장 필드가 반환됩니다.