Skip to content

feat: 외부 도메인 발급을 신청 흐름으로 편입 - #132

Merged
yessjun merged 12 commits into
mainfrom
feat/domain-request-kind
Sep 17, 2026
Merged

yessjun merged 12 commits into
mainfrom
feat/domain-request-kind

Conversation

@yessjun

@yessjun yessjun commented Sep 16, 2026

Copy link
Copy Markdown
Member

📝 작업내용

  • 외부 도메인 발급을 신청 흐름의 네 번째 종류로 편입
    • POST /dns-domains 폐지, 발급은 POST /requestsdomain 항목으로 접수
    • 신청 세부는 domain_request_details에 적재하고 승인 시점에 이름을 발급 (V124)
    • assert_approved_request_is_granted()에 DOMAIN 분기 추가, 분기가 없으면 승인 커밋이 실패
  • 승인 여부를 루트 도메인마다의 정책으로 도입 (V122)
    • domain_roots.auto_approve가 true면 접수와 동시에 승인, false면 기존 승인 큐
    • 기본값 true라 배포 시점 동작은 그대로
  • 자동 승인의 검토 기록에서 결재자 제거 (V123)
    • request_reviews.reviewer_id NOT NULL 해제, 외래 키는 유지
    • 결재자 이름은 「자동 승인」, 계정 행이 사라진 경우의 「탈퇴 회원」과 구분
  • 자동 승인과 사람의 승인이 같은 본문을 실행하도록 RequestApproval로 분리
  • 되살리기를 전용 경로 POST /dns-domains/{domainId}/revive로 신설
    • 부여가 없는 워크스페이스 구성원이 되살리면 행은 살아나고 응답이 404이던 결함 수정
  • 관리자 연산 4건 추가
    • 루트 목록 조회와 승인 정책 변경, 도메인 레코드 열람, 사용 기한 조정
    • 사용 기한을 지난 시각으로 옮기는 요청은 거절하고, 변경 시 소유자에게 통지
  • CreateRequest.orgId를 선택 항목으로 변경
    • 기관을 스스로 정하는 종류는 owningOrgId로 유도하고, 나머지는 종전대로 필수
    • 기관 해석을 종류별 검증 뒤로 이동
  • AdminDomainView.vmIdvmName을 nullable로 정정, 외부 도메인 행은 이미 null 전송
  • API 명세 v0.81.0
  • 적대 검토가 낸 결함 정정
    • 레코드 열람이 쓰기용 스코프 검사를 타 기관 열람자가 자기 기관 이름에 404를 받던 것
    • 기관 누락이 다른 검증 오류와 같은 응답에 실리지 않고 왕복을 한 번 더 요구하던 것
    • 루트 행의 updated_at이 갱신되지 않아 마지막 정책 변경 시각을 참칭하던 것
    • 승인 경로가 신청서에만 있는 필드 이름으로 422를 내던 것
    • V124 가드가 치환 토큰의 유일성을 확인하지 않던 것

⭐️ 검증

  • 테스트 1551개 통과, 빌드와 의존성 감사
  • 신규 테스트 21건이 답하는 것
    • 자동 루트는 접수와 동시에 승인되고 검토 기록에 결재자가 없음
    • 승인 필요 루트는 SUBMITTED로 남아 승인 큐에 표시
    • 같은 라벨의 두 번째 신청이 제출 단계에서 거절
    • 승인 시점에 이름 중복과 상한, 루트와 기관 상태를 잠금 아래에서 재검사
    • 상한이 발급된 이름과 검토 대기 신청을 함께 계산
    • 기관 등급 관리자가 다른 기관 루트의 정책을 바꾸면 404
    • 루트 행에 auto_approve 컬럼을 넣지 않고 삽입해 기본값 자체를 검증
    • 기관 열람자가 자기 기관 레코드는 읽고 사용 기한은 바꾸지 못함
  • 열람자 결함은 변이 검사로 확인, 읽기/쓰기 분기를 되돌리면 신규 테스트가 그 404로 실패
  • 권한 행렬과 집행 픽스처 1:1 대조 통과

💬 리뷰 포인트

  • 승인 여부를 기관이 아니라 루트 도메인에 둔 단위
  • 기관 해석을 종류에 위임하면서 orgId를 선택으로 바꾼 본문 모양
  • 되살리기를 신청 흐름 밖에 남긴 경계
  • GET /admin/domain-roots를 기관으로 좁히지 않은 범위

@yessjun
yessjun force-pushed the feat/domain-request-kind branch from 0bc39ef to 01ab0ee Compare September 17, 2026 06:09
The domain kind takes its organisation from the root it is asked under, so
asking the applicant for one too would let a form carry two answers with
nothing downstream able to tell which was meant. orgId is now optional and
resolveOrg() asks the handler first, falling back to the field for every
other kind and refusing when neither supplies one.

The call moved after the kind's own validation: working the organisation
out of a root that does not exist would answer with that failure instead
of the field error the applicant needs to read.

AdminDomainView.vmId and vmName become nullable in the same unit. External
names have no virtual machine and the rows were already going out null, so
the contract was claiming something the server does not send.
Reading a name's records asked whether the account may act in the
organisation, because every previous caller of that scope check was a write
and a write is what it was built for. An organisation viewer got a 404 for
its own institution's names: the row stood in the listing and the drawer
refused to open it.

The resolution splits in two. A write still asks operates(); a read asks
reads(), which is every organisation the account holds any role in. The two
sets differ only once a read-only role exists, which is exactly why the
distinction is written down on AuthenticatedUser and why collapsing them
here went unnoticed.

Both tests fail without the fix, and the one that matters fails with the
404 the viewer was getting.
Making orgId optional moved its refusal from the annotation into the
service, and the service ran after the kind had already thrown. A body
missing the organisation and something else got told about the something
else, then about the organisation on the next attempt: two round trips
where the annotation had managed one.

Whether the field is required is knowable without reading the form, so it
is asked first and joins the same 422 as every other missing field. Which
organisation can only be worked out after the kind's own fields are known
good, because for a name it means reading the root that was named, so that
stays where it is.

This platform's own console never produced the shape, since it refuses all
four fields client-side, which is the reason it would have gone unnoticed.
Six statements the round made false or left imprecise, none of which any
gate could catch.

DnsDomainService still opened with "no approval anywhere in here", which is
the position this round reversed; the root now decides. A javadoc block
whose method had moved to DomainIssuancePolicy was left behind with nothing
under it. CreateRequestRequest claimed the service refuses a body whose
type and nested member disagree, and no such check has ever existed.

The renewal policy's root row never moved updated_at, harmless while
nothing could edit a root and a false claim the moment the policy became
editable. An approval that met a root deleted since submission answered a
field error naming a box on the request form, where the reviewer has no
such box; it answers the conflict its sibling refusal already answered.

V124's guard asserted the replacement token exists but not that it is
unique, while replace() is global. V122's default said why existing rows
stay open and not why new ones do, which is a separate decision.
A round merged while this one was open and took 0.79.0, so the version this
branch publishes moves up rather than down. The spec is regenerated from the
rebased tree and the permission fixture's header follows it.

The rebase resolved the generated spec by taking main's copy, on the
understanding that regenerating would replace it. Regenerating happened and
committing it did not, so the tree carried a spec that disagreed with the
version constant beside it.
The version this branch was holding landed on main from another round, so
this one moves up rather than publishing a number that is already taken.
The spec is regenerated from the rebased tree and the permission fixture's
header follows it.
@yessjun
yessjun force-pushed the feat/domain-request-kind branch from d62a873 to b0ea97e Compare September 17, 2026 06:59
@yessjun
yessjun merged commit 1a78223 into main Sep 17, 2026
1 check passed
@yessjun
yessjun deleted the feat/domain-request-kind branch September 17, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant