1
0
forked from baron/baron-sso
Files
baron-sso/docs/custom-field-jsonb-index-policy.md

8.5 KiB

Custom Field JSONB 및 인덱스 정책

현재 구조

  • Tenant custom schema는 tenants.config.userSchema JSONB에 저장한다.
  • Tenant custom value는 backend DB의 users.metadata JSONB에 저장한다.
  • isLoginId=true인 Tenant field 값은 로그인 식별자 처리를 위해 user_login_ids에도 동기화한다.
  • Ory Kratos traits에는 인증/식별에 필요한 최소 값만 동기화하는 방향으로 정리한다.
  • RP custom value는 backend DB의 rp_user_metadata.metadata JSONB에 별도 저장한다.

Tenant Custom Field

Tenant schema field는 다음 속성을 기준으로 한다.

{
  "key": "employeeNo",
  "label": "사번",
  "type": "text",
  "required": false,
  "indexed": true,
  "isLoginId": true,
  "adminOnly": false,
  "validation": "^[A-Z0-9]+$"
}
  • indexed=true는 검색/필터 최적화 대상이라는 의미다.
  • isLoginId=true이면 backend와 adminfront 모두 indexed=true를 강제한다.
  • isLoginId=true는 값 필수를 의미하지 않는다. 값 필수 여부는 required=true로 별도 제어한다.
  • isLoginId=true인 field는 type=text만 허용한다.
  • JSONB 통합 정책에서는 varchar 크기 지정 의미를 두지 않는다.

RP Custom Field

RP custom schema는 client metadata의 customUserSchema에 저장한다.

{
  "customUserSchema": [
    {
      "key": "approvalLevel",
      "label": "승인 등급",
      "type": "text",
      "required": false,
      "indexed": true,
      "claimEnabled": true
    }
  ]
}

RP custom value는 rp_user_metadata 테이블에 저장한다.

client_id text
user_id uuid
metadata jsonb
created_at timestamptz
updated_at timestamptz
primary key (client_id, user_id)
foreign key (user_id) references users(id)

Backend API 초안:

GET /api/v1/dev/clients/:id/users/:userId/metadata
PUT /api/v1/dev/clients/:id/users/:userId/metadata

PUT payload:

{
  "metadata": {
    "approvalLevel": "A",
    "preferences": {
      "theme": "dark"
    }
  }
}

검색 및 인덱스

  • indexed=true field만 검색 UI/API 후보로 노출한다.
  • 기본 검색은 exact match, exists, JSON containment 중심으로 제한한다.
  • RP custom field의 LIKE/fuzzy 검색은 기본 제공하지 않는다.
  • GIN 인덱스는 backend index manager가 별도 상태로 관리하는 방향을 원칙으로 한다.
  • API 요청 처리 중 CREATE INDEX를 동기 실행하지 않는다.

Claim Projection

JWT 또는 userinfo 응답에서는 custom field를 top-level에 풀지 않는다. Tenant/RP 단위로 묶어서 전달한다.

{
  "tenant_profiles": [
    {
      "tenant_id": "01970f0a-5c28-74d8-a73a-f6e9e9a7b210",
      "tenant_slug": "hanmac-family",
      "fields": {
        "employeeNo": "E1001"
      }
    }
  ],
  "rp_profiles": [
    {
      "client_id": "sample-rp",
      "fields": {
        "approvalLevel": "A"
      }
    }
  ]
}
  • claimEnabled=true field만 RP claim 후보로 포함한다.
  • 긴 JSON 값은 기본적으로 token claim보다 userinfo/profile API 응답에 싣는 방향을 우선한다.

한맥가족 Tenant Claim Projection

한맥가족(hanmac-family) subtree의 tenant claim은 기본 claim과 상세 claim으로 나눈다. 기본 claim은 대표소속 tenant UUID인 tenant_id와 전체 소속 목록인 joined_tenants이며, RP가 tenant claim을 요청하면 tenant별 map 안에 조직 소속 정보를 묶어서 전달한다. 이 정보는 RP가 tenant context를 표시하거나 조직별 기본값을 선택하기 위한 projection이며, 관계형 데이터의 SoT는 PostgreSQL Business DB와 사용자 metadata이다.

기본 claim 예시는 다음과 같다.

{
  "tenant_id": "01970f0a-5c28-74d8-a73a-f6e9e9a7b210",
  "joined_tenants": [
    "01970f0a-5c28-74d8-a73a-f6e9e9a7b210",
    "01970f0b-3448-7bb8-bdc7-16b6a1d2e661"
  ]
}

Issue #775 구현 결과 기준으로 RP가 tenant claim을 요청했을 때 받는 대표 예시는 다음과 같다.

{
  "email": "hanmac-user@example.com",
  "name": "한맥 사용자",
  "tenant_id": "01970f0a-5c28-74d8-a73a-f6e9e9a7b210",
  "joined_tenants": [
    "01970f0a-5c28-74d8-a73a-f6e9e9a7b210",
    "01970f0b-3448-7bb8-bdc7-16b6a1d2e661"
  ],
  "lead_tenants": [
    "01970f0a-5c28-74d8-a73a-f6e9e9a7b210"
  ],
  "tenants": {
    "01970f0a-5c28-74d8-a73a-f6e9e9a7b210": {
      "id": "01970f0a-5c28-74d8-a73a-f6e9e9a7b210",
      "slug": "tech-planning",
      "name": "기술기획팀",
      "type": "USER_GROUP",
      "lead": true,
      "representative": true,
      "isPrimary": true,
      "grade": "책임",
      "jobTitle": "기술기획",
      "position": "팀장",
      "parentTenantId": "01970f08-91da-7286-bd19-882fb98d1f2c",
      "ancestors": [
        {
          "id": "01970f08-91da-7286-bd19-882fb98d1f2c",
          "slug": "hanmac",
          "name": "한맥기술",
          "type": "COMPANY",
          "parentTenantId": "01970f07-4f01-7d9a-a71e-b53ad508f345"
        },
        {
          "id": "01970f07-4f01-7d9a-a71e-b53ad508f345",
          "slug": "hanmac-family",
          "name": "한맥가족",
          "type": "COMPANY_GROUP",
          "parentTenantId": null
        }
      ]
    },
    "01970f0b-3448-7bb8-bdc7-16b6a1d2e661": {
      "id": "01970f0b-3448-7bb8-bdc7-16b6a1d2e661",
      "slug": "quality",
      "name": "품질관리팀",
      "type": "USER_GROUP",
      "lead": false,
      "representative": false,
      "isPrimary": false,
      "grade": "선임",
      "jobTitle": "품질관리",
      "position": "파트원",
      "parentTenantId": "01970f08-91da-7286-bd19-882fb98d1f2c",
      "ancestors": [
        {
          "id": "01970f08-91da-7286-bd19-882fb98d1f2c",
          "slug": "hanmac",
          "name": "한맥기술",
          "type": "COMPANY",
          "parentTenantId": "01970f07-4f01-7d9a-a71e-b53ad508f345"
        },
        {
          "id": "01970f07-4f01-7d9a-a71e-b53ad508f345",
          "slug": "hanmac-family",
          "name": "한맥가족",
          "type": "COMPANY_GROUP",
          "parentTenantId": null
        }
      ]
    }
  },
  "profile": {
    "emails": [
      "hanmac-user@example.com"
    ],
    "names": {
      "name": "한맥 사용자"
    }
  }
}
  • 예시의 id 값은 UUID 형식의 샘플이며, slug와 다르다.
  • tenant_idjoined_tenants는 기본 claim이다.
  • tenant_id는 사용자의 대표소속 tenant UUID이다. RP/client context tenant가 없더라도 공백으로 내려가지 않는다.
  • joined_tenants는 사용자가 claim 상에서 소속된 모든 tenant UUID 목록이다.
  • lead_tenantstenant claim 요청 시 포함되며, lead=true인 tenant UUID 목록이다.
  • lead는 tenant lead/조직장 역할을 나타낸다. 입력 metadata에서는 lead, isLead, isOwner, isManager를 허용한다.
  • representativeisPrimary는 대표조직 여부를 나타낸다. 입력 metadata에서는 representative, isPrimary, primary를 허용한다.
  • grade, jobTitle, position은 각각 직급, 직무, 직책이다.
  • parentTenantId는 현재 tenant의 직속 parent tenant UUID이다. 최상위 root는 null이다.
  • ancestors는 직속 상위 tenant부터 hanmac-family root까지의 parent chain이다.
  • 기본 tenant와 각 ancestor 객체는 parentTenantId를 포함하므로, parent edge를 별도 추론 없이 그릴 수 있다.
  • 대표소속 결정은 명시적 tenant_id, additionalAppointmentsrepresentative/isPrimary/primary=true, 가장 먼저 등록된 소속 순서로 적용한다.
  • 생성 시 소속 tenant가 하나도 없으면 PERSONAL tenant를 자동 생성하고, 해당 tenant를 tenant_idjoined_tenants에 포함한다.
  • RP/client tenant context는 대표소속 tenant_id를 덮어쓰지 않는다.
  • tenant별 namespaced traits map이 없어도 tenant_id 또는 additionalAppointments[].tenantId를 기준으로 projection 항목을 만들 수 있다.
  • 멀티 소속이면 기본 claim의 joined_tenants에 모든 소속 tenant를 넣는다. tenant claim 요청 시에는 tenants에도 모든 소속 tenant 상세를 넣고, lead_tenants에는 lead tenant만 넣는다.
  • token 크기 보호를 위해 전체 조직도나 긴 custom JSON은 claim에 싣지 않고 profile/userinfo API 또는 backend API 응답으로 분리한다.
  • RP는 joined_tenants로 전체 소속을 읽고, lead_tenants로 lead tenant를 빠르게 식별한다. 상세 표시는 tenants[tenant_id] 또는 tenants[joined_tenants[n]]ancestors를 조합한다.