@things-factory/auth-base 10.1.278 → 10.1.280
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +24 -7
- package/CHANGELOG.md +0 -55
- package/dist-client/actions/auth.js.map +0 -1
- package/dist-client/apply-domain-theme.js.map +0 -1
- package/dist-client/auth.js.map +0 -1
- package/dist-client/bootstrap.js.map +0 -1
- package/dist-client/directive/privileged.js.map +0 -1
- package/dist-client/domain-required.js.map +0 -1
- package/dist-client/index.js.map +0 -1
- package/dist-client/profiled.js.map +0 -1
- package/dist-client/reducers/auth.js.map +0 -1
- package/dist-client/tsconfig.tsbuildinfo +0 -1
- package/dist-client/verify-webauthn.js.map +0 -1
- package/dist-server/ai-entry/access-assistant.js.map +0 -1
- package/dist-server/ai-entry/access-entry-targets.js.map +0 -1
- package/dist-server/ai-entry/entry-target-shape.js.map +0 -1
- package/dist-server/constants/error-code.js.map +0 -1
- package/dist-server/constants/error-message.js.map +0 -1
- package/dist-server/constants/max-age.js.map +0 -1
- package/dist-server/controllers/access-request.js.map +0 -1
- package/dist-server/controllers/account-mail.js.map +0 -1
- package/dist-server/controllers/auth.js.map +0 -1
- package/dist-server/controllers/change-pwd.js.map +0 -1
- package/dist-server/controllers/confirm-identity.js.map +0 -1
- package/dist-server/controllers/delete-user.js.map +0 -1
- package/dist-server/controllers/passkey.js.map +0 -1
- package/dist-server/controllers/password-setup.js.map +0 -1
- package/dist-server/controllers/profile.js.map +0 -1
- package/dist-server/controllers/reset-password.js.map +0 -1
- package/dist-server/controllers/signin.js.map +0 -1
- package/dist-server/controllers/signup.js.map +0 -1
- package/dist-server/controllers/unlock-user.js.map +0 -1
- package/dist-server/controllers/utils/find-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/make-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/password-rule.js.map +0 -1
- package/dist-server/controllers/utils/save-verification-token.js.map +0 -1
- package/dist-server/controllers/verification.js.map +0 -1
- package/dist-server/errors/auth-error.js.map +0 -1
- package/dist-server/errors/index.js.map +0 -1
- package/dist-server/errors/user-domain-not-match-error.js.map +0 -1
- package/dist-server/index.js.map +0 -1
- package/dist-server/middlewares/audit-scope-middleware.js.map +0 -1
- package/dist-server/middlewares/authenticate-401-middleware.js.map +0 -1
- package/dist-server/middlewares/bypass-signin-middleware.js.map +0 -1
- package/dist-server/middlewares/domain-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/graphql-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/index.js.map +0 -1
- package/dist-server/middlewares/jwt-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/signin-middleware.js.map +0 -1
- package/dist-server/middlewares/verify-recaptcha-middleware.js.map +0 -1
- package/dist-server/migrations/1548206416130-SeedUser.js.map +0 -1
- package/dist-server/migrations/1566805283882-SeedPrivilege.js.map +0 -1
- package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.js.map +0 -1
- package/dist-server/migrations/index.js.map +0 -1
- package/dist-server/role-tasks.js.map +0 -1
- package/dist-server/router/auth-checkin-router.js.map +0 -1
- package/dist-server/router/auth-private-process-router.js.map +0 -1
- package/dist-server/router/auth-public-process-router.js.map +0 -1
- package/dist-server/router/auth-signin-router.js.map +0 -1
- package/dist-server/router/auth-signup-router.js.map +0 -1
- package/dist-server/router/index.js.map +0 -1
- package/dist-server/router/oauth2/index.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-authorize-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-server.js.map +0 -1
- package/dist-server/router/oauth2/passport-oauth2-client-password.js.map +0 -1
- package/dist-server/router/oauth2/passport-refresh-token.js.map +0 -1
- package/dist-server/router/path-base-domain-router.js.map +0 -1
- package/dist-server/router/site-root-router.js.map +0 -1
- package/dist-server/router/webauthn-router.js.map +0 -1
- package/dist-server/routes.js.map +0 -1
- package/dist-server/service/access-request/access-request-mutation.js.map +0 -1
- package/dist-server/service/access-request/access-request-query.js.map +0 -1
- package/dist-server/service/access-request/access-request-types.js.map +0 -1
- package/dist-server/service/access-request/access-request.js.map +0 -1
- package/dist-server/service/access-request/index.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-mutation.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-query.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-types.js.map +0 -1
- package/dist-server/service/app-binding/app-binding.js.map +0 -1
- package/dist-server/service/app-binding/index.js.map +0 -1
- package/dist-server/service/appliance/appliance-mutation.js.map +0 -1
- package/dist-server/service/appliance/appliance-query.js.map +0 -1
- package/dist-server/service/appliance/appliance-types.js.map +0 -1
- package/dist-server/service/appliance/appliance.js.map +0 -1
- package/dist-server/service/appliance/index.js.map +0 -1
- package/dist-server/service/application/application-mutation.js.map +0 -1
- package/dist-server/service/application/application-query.js.map +0 -1
- package/dist-server/service/application/application-types.js.map +0 -1
- package/dist-server/service/application/application.js.map +0 -1
- package/dist-server/service/application/index.js.map +0 -1
- package/dist-server/service/audit/audit-action.js.map +0 -1
- package/dist-server/service/audit/audit-kinds.js.map +0 -1
- package/dist-server/service/audit/audit-query.js.map +0 -1
- package/dist-server/service/audit/audit-recorder.js.map +0 -1
- package/dist-server/service/audit/audit-request.js.map +0 -1
- package/dist-server/service/audit/audit-retention.js.map +0 -1
- package/dist-server/service/audit/audit-subscriber.js.map +0 -1
- package/dist-server/service/audit/audit-target.js.map +0 -1
- package/dist-server/service/audit/audit-types.js.map +0 -1
- package/dist-server/service/audit/audit-work.js.map +0 -1
- package/dist-server/service/audit/index.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-mutation.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-parameter-spec.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-query.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-type.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider.js.map +0 -1
- package/dist-server/service/auth-provider/index.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-mutation.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-types.js.map +0 -1
- package/dist-server/service/domain-generator/index.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-mutation.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-query.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-types.js.map +0 -1
- package/dist-server/service/domain-link/domain-link.js.map +0 -1
- package/dist-server/service/domain-link/index.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-mutation.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-query.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner.js.map +0 -1
- package/dist-server/service/domain-owner/index.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-mutation.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-query.js.map +0 -1
- package/dist-server/service/granted-role/granted-role.js.map +0 -1
- package/dist-server/service/granted-role/index.js.map +0 -1
- package/dist-server/service/identity-confirmation/identity-confirmation.js.map +0 -1
- package/dist-server/service/index.js.map +0 -1
- package/dist-server/service/open-fields.js.map +0 -1
- package/dist-server/service/partner/index.js.map +0 -1
- package/dist-server/service/partner/partner-mutation.js.map +0 -1
- package/dist-server/service/partner/partner-query.js.map +0 -1
- package/dist-server/service/partner/partner-types.js.map +0 -1
- package/dist-server/service/partner/partner.js.map +0 -1
- package/dist-server/service/password-history/index.js.map +0 -1
- package/dist-server/service/password-history/password-history.js.map +0 -1
- package/dist-server/service/privilege/index.js.map +0 -1
- package/dist-server/service/privilege/open-field-rule.js.map +0 -1
- package/dist-server/service/privilege/privilege-axis.js.map +0 -1
- package/dist-server/service/privilege/privilege-deprecation.js.map +0 -1
- package/dist-server/service/privilege/privilege-directive.js.map +0 -1
- package/dist-server/service/privilege/privilege-mutation.js.map +0 -1
- package/dist-server/service/privilege/privilege-query.js.map +0 -1
- package/dist-server/service/privilege/privilege-rejection.js.map +0 -1
- package/dist-server/service/privilege/privilege-sync.js.map +0 -1
- package/dist-server/service/privilege/privilege-types.js.map +0 -1
- package/dist-server/service/privilege/privilege.js.map +0 -1
- package/dist-server/service/role/index.js.map +0 -1
- package/dist-server/service/role/role-mutation.js.map +0 -1
- package/dist-server/service/role/role-privilege-query.js.map +0 -1
- package/dist-server/service/role/role-query.js.map +0 -1
- package/dist-server/service/role/role-types.js.map +0 -1
- package/dist-server/service/role/role.js.map +0 -1
- package/dist-server/service/role-template/index.js.map +0 -1
- package/dist-server/service/role-template/role-task.js.map +0 -1
- package/dist-server/service/role-template/role-template-gap-resolver.js.map +0 -1
- package/dist-server/service/role-template/role-template-mutation.js.map +0 -1
- package/dist-server/service/role-template/role-template-query.js.map +0 -1
- package/dist-server/service/role-template/role-template-types.js.map +0 -1
- package/dist-server/service/role-template/role-template.js.map +0 -1
- package/dist-server/service/user/domain-query.js.map +0 -1
- package/dist-server/service/user/index.js.map +0 -1
- package/dist-server/service/user/user-mutation.js.map +0 -1
- package/dist-server/service/user/user-query.js.map +0 -1
- package/dist-server/service/user/user-types.js.map +0 -1
- package/dist-server/service/user/user.js.map +0 -1
- package/dist-server/service/users-auth-providers/index.js.map +0 -1
- package/dist-server/service/users-auth-providers/users-auth-providers.js.map +0 -1
- package/dist-server/service/verification-token/index.js.map +0 -1
- package/dist-server/service/verification-token/verification-token.js.map +0 -1
- package/dist-server/service/web-auth-credential/index.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-query.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential.js.map +0 -1
- package/dist-server/tsconfig.tsbuildinfo +0 -1
- package/dist-server/types.js.map +0 -1
- package/dist-server/utils/accepts.js.map +0 -1
- package/dist-server/utils/access-request-review.js.map +0 -1
- package/dist-server/utils/access-token-cookie.js.map +0 -1
- package/dist-server/utils/account-lock-rule.js.map +0 -1
- package/dist-server/utils/assignable-roles.js.map +0 -1
- package/dist-server/utils/build-auth-url.js.map +0 -1
- package/dist-server/utils/can-user-checkin-domain.js.map +0 -1
- package/dist-server/utils/check-permission.js.map +0 -1
- package/dist-server/utils/check-user-belongs-domain.js.map +0 -1
- package/dist-server/utils/check-user-has-role.js.map +0 -1
- package/dist-server/utils/checkin-domain-facts.js.map +0 -1
- package/dist-server/utils/checkin-domain-rule.js.map +0 -1
- package/dist-server/utils/credential-at-rest.js.map +0 -1
- package/dist-server/utils/credential-features.js.map +0 -1
- package/dist-server/utils/credential-serial-rule.js.map +0 -1
- package/dist-server/utils/domain-ownership.js.map +0 -1
- package/dist-server/utils/encrypt-state.js.map +0 -1
- package/dist-server/utils/get-aes-256-key.js.map +0 -1
- package/dist-server/utils/get-domain-users.js.map +0 -1
- package/dist-server/utils/get-secret.js.map +0 -1
- package/dist-server/utils/get-user-domains.js.map +0 -1
- package/dist-server/utils/lock-duration.js.map +0 -1
- package/dist-server/utils/password-reset-rule.js.map +0 -1
- package/dist-server/utils/profile-user.js.map +0 -1
- package/dist-server/utils/refuse-role-name.js.map +0 -1
- package/dist-server/utils/role-name-standing.js.map +0 -1
- package/dist-server/utils/same-origin-redirect.js.map +0 -1
- package/dist-server/utils/verification-token-rule.js.map +0 -1
- package/spec/unit/checkin-domain.spec.ts +0 -307
- package/spec/unit/checkin-privilege.spec.ts +0 -205
- package/spec/unit/domain-owner-entity.spec.ts +0 -179
- package/spec/unit/domain-owner-granted.spec.ts +0 -215
- package/spec/unit/domain-owner-migration.spec.ts +0 -236
- package/spec/unit/domain-owner-mutation.spec.ts +0 -337
- package/spec/unit/domain-ownership.spec.ts +0 -188
- package/tests/access-assistant-db.test.ts +0 -155
- package/tests/access-entry-targets-db.test.ts +0 -176
- package/tests/access-request-db.test.ts +0 -394
- package/tests/access-request-pg.test.ts +0 -83
- package/tests/access-request-review.test.ts +0 -193
- package/tests/account-lock-db.test.ts +0 -216
- package/tests/account-lock-rule.test.ts +0 -141
- package/tests/account-mail.test.ts +0 -69
- package/tests/activate-page-case.test.ts +0 -53
- package/tests/add-user-gates.test.ts +0 -34
- package/tests/app-binding-db.test.ts +0 -210
- package/tests/app-binding-delete-db.test.ts +0 -188
- package/tests/appliance-credential-state-db.test.ts +0 -207
- package/tests/appliance-delete-db.test.ts +0 -119
- package/tests/appliance-revoke-db.test.ts +0 -294
- package/tests/appliance-schema.test.ts +0 -129
- package/tests/application-delete-db.test.ts +0 -190
- package/tests/application-token-payload-db.test.ts +0 -185
- package/tests/assignable-roles-db.test.ts +0 -219
- package/tests/audit-db.test.ts +0 -410
- package/tests/audit-work.test.ts +0 -58
- package/tests/auth-base-role-tasks.test.ts +0 -58
- package/tests/checkin-domain-rule.test.ts +0 -128
- package/tests/checkin-privilege-seam-db.test.ts +0 -158
- package/tests/compiled.ts +0 -41
- package/tests/confirm-identity-db.test.ts +0 -85
- package/tests/credential-at-rest-db.test.ts +0 -246
- package/tests/credential-features-off-db.test.ts +0 -189
- package/tests/credential-serial-rule.test.ts +0 -84
- package/tests/declare-privileges.test.ts +0 -29
- package/tests/domain-assignees-db.test.ts +0 -95
- package/tests/domain-inheritance-sentinel.test.ts +0 -93
- package/tests/domain-required.test.ts +0 -127
- package/tests/has-privilege-db.test.ts +0 -198
- package/tests/identity-confirmation.test.ts +0 -146
- package/tests/list-inheritance-db.test.ts +0 -127
- package/tests/lock-recovery.test.ts +0 -206
- package/tests/mail-request-answers-db.test.ts +0 -142
- package/tests/open-doors.test.ts +0 -121
- package/tests/open-fields-enforcement.test.ts +0 -215
- package/tests/open-fields-framework.test.ts +0 -212
- package/tests/open-redirect-db.test.ts +0 -143
- package/tests/passkey-authenticator.ts +0 -104
- package/tests/passkey-db.test.ts +0 -295
- package/tests/passkey-signin-route-db.test.ts +0 -139
- package/tests/password-hashing.test.ts +0 -131
- package/tests/password-persistence.test.ts +0 -147
- package/tests/password-reset-rule-db.test.ts +0 -159
- package/tests/password-setup-db.test.ts +0 -215
- package/tests/permission-gate.test.ts +0 -228
- package/tests/picture-reducer.test.ts +0 -25
- package/tests/privilege-axis.test.ts +0 -157
- package/tests/privilege-deprecation.test.ts +0 -65
- package/tests/privilege-directive.test.ts +0 -332
- package/tests/privilege-rejection.test.ts +0 -104
- package/tests/privilege-sync-db.test.ts +0 -58
- package/tests/profile-user.test.ts +0 -24
- package/tests/refusal-cause.test.ts +0 -80
- package/tests/refusals-have-korean.test.ts +0 -24
- package/tests/request-holds.test.ts +0 -53
- package/tests/role-inheritance-db.test.ts +0 -286
- package/tests/role-mutation-db.test.ts +0 -293
- package/tests/role-name-lookup-sentinel.test.ts +0 -79
- package/tests/role-name-standing.test.ts +0 -83
- package/tests/role-privileges-db.test.ts +0 -252
- package/tests/role-task.test.ts +0 -168
- package/tests/role-template-gap-db.test.ts +0 -268
- package/tests/role-template-seed-db.test.ts +0 -180
- package/tests/role-template.test.ts +0 -162
- package/tests/role-users-gate.test.ts +0 -22
- package/tests/signin-policy.test.ts +0 -200
- package/tests/signup-duplicate-db.test.ts +0 -68
- package/tests/token-issuance.test.ts +0 -244
- package/tests/user-roles-field-db.test.ts +0 -134
- package/tests/verification-token-db.test.ts +0 -285
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DomainOwner Mutation 로직 테스트
|
|
3
|
-
*
|
|
4
|
-
* addDomainOwner / removeDomainOwner mutation 의 핵심 비즈니스 로직
|
|
5
|
-
* (멤버 확인, 중복 방지, 마지막 오너 보호, legacy cache 동기화)을
|
|
6
|
-
* 테스트 엔티티 스키마로 재현해 검증한다.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { EntityManager } from 'typeorm'
|
|
10
|
-
import { TestDatabase } from '../../../../test/test-database'
|
|
11
|
-
import {
|
|
12
|
-
domainFactory,
|
|
13
|
-
userFactory,
|
|
14
|
-
domainOwnerFactory
|
|
15
|
-
} from '../../../../test/factories'
|
|
16
|
-
import { Domain, DomainOwner, User } from '../../../../test/entities/schemas'
|
|
17
|
-
|
|
18
|
-
// ============================================================
|
|
19
|
-
// 테스트용 헬퍼: source 로직을 테스트 스키마로 재현
|
|
20
|
-
// ============================================================
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* addDomainOwner 의 핵심 로직
|
|
24
|
-
* - 대상 유저가 domain 멤버인지 확인
|
|
25
|
-
* - 이미 owner인지 확인
|
|
26
|
-
* - 엔트리 생성
|
|
27
|
-
* - Domain.owner 캐시가 비어있으면 세팅
|
|
28
|
-
*/
|
|
29
|
-
async function addDomainOwnerLogic(
|
|
30
|
-
manager: EntityManager,
|
|
31
|
-
domain: Domain,
|
|
32
|
-
targetUser: User,
|
|
33
|
-
actor: User,
|
|
34
|
-
options: { isMember: boolean; reason?: string }
|
|
35
|
-
): Promise<DomainOwner> {
|
|
36
|
-
if (!options.isMember) {
|
|
37
|
-
throw new Error(`User '${targetUser.name}' is not a member of this domain.`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const ownerRepo = manager.getRepository<DomainOwner>('DomainOwner')
|
|
41
|
-
|
|
42
|
-
const existing = await ownerRepo.findOne({
|
|
43
|
-
where: { domain: { id: domain.id }, user: { id: targetUser.id } }
|
|
44
|
-
})
|
|
45
|
-
if (existing) {
|
|
46
|
-
throw new Error(`User '${targetUser.name}' is already an owner of this domain.`)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const entry = ownerRepo.create({
|
|
50
|
-
domain: { id: domain.id } as Domain,
|
|
51
|
-
user: { id: targetUser.id } as User,
|
|
52
|
-
grantedBy: actor,
|
|
53
|
-
reason: options.reason
|
|
54
|
-
} as Partial<DomainOwner>)
|
|
55
|
-
const saved = await ownerRepo.save(entry)
|
|
56
|
-
|
|
57
|
-
if (!domain.owner) {
|
|
58
|
-
await manager.update('Domain', domain.id, { owner: targetUser.id })
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return saved
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* removeDomainOwner 의 핵심 로직
|
|
66
|
-
* - 대상 유저의 엔트리 혹은 legacy owner 여부 확인
|
|
67
|
-
* - 남을 owner가 1명 이상 보장
|
|
68
|
-
* - legacy cache가 제거 대상이면 남은 오너 중 하나로 재설정
|
|
69
|
-
*/
|
|
70
|
-
async function removeDomainOwnerLogic(
|
|
71
|
-
manager: EntityManager,
|
|
72
|
-
domain: Domain,
|
|
73
|
-
targetUser: User
|
|
74
|
-
): Promise<boolean> {
|
|
75
|
-
const ownerRepo = manager.getRepository<DomainOwner>('DomainOwner')
|
|
76
|
-
|
|
77
|
-
const entry = await ownerRepo.findOne({
|
|
78
|
-
where: { domain: { id: domain.id }, user: { id: targetUser.id } }
|
|
79
|
-
})
|
|
80
|
-
const hasLegacyOwner = domain.owner === targetUser.id
|
|
81
|
-
|
|
82
|
-
if (!entry && !hasLegacyOwner) {
|
|
83
|
-
throw new Error(`User '${targetUser.name}' is not an owner of this domain.`)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const ownerCount = await ownerRepo.count({
|
|
87
|
-
where: { domain: { id: domain.id } }
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
let remaining = ownerCount
|
|
91
|
-
if (entry) remaining -= 1
|
|
92
|
-
if (domain.owner && domain.owner !== targetUser.id) {
|
|
93
|
-
const legacyInTable = await ownerRepo.findOne({
|
|
94
|
-
where: { domain: { id: domain.id }, user: { id: domain.owner } }
|
|
95
|
-
})
|
|
96
|
-
if (!legacyInTable) remaining += 1
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (remaining < 1) {
|
|
100
|
-
throw new Error('Cannot remove the last owner of a domain. Add another owner first.')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (entry) {
|
|
104
|
-
await ownerRepo.delete(entry.id)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (hasLegacyOwner) {
|
|
108
|
-
const next = await ownerRepo.findOne({
|
|
109
|
-
where: { domain: { id: domain.id } },
|
|
110
|
-
order: { grantedAt: 'ASC' }
|
|
111
|
-
})
|
|
112
|
-
const nextOwnerId = next
|
|
113
|
-
? (await manager.findOne<DomainOwner>('DomainOwner', {
|
|
114
|
-
where: { id: next.id },
|
|
115
|
-
relations: ['user']
|
|
116
|
-
}))?.user.id ?? null
|
|
117
|
-
: null
|
|
118
|
-
await manager.update('Domain', domain.id, { owner: nextOwnerId })
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return true
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ============================================================
|
|
125
|
-
// 테스트
|
|
126
|
-
// ============================================================
|
|
127
|
-
|
|
128
|
-
describe('addDomainOwner 로직', () => {
|
|
129
|
-
let testDb: TestDatabase
|
|
130
|
-
|
|
131
|
-
beforeAll(() => {
|
|
132
|
-
testDb = TestDatabase.getInstance()
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('도메인 멤버를 owner로 추가할 수 있다', async () => {
|
|
136
|
-
const ds = testDb.getDataSource()
|
|
137
|
-
const domain = await domainFactory.create({})
|
|
138
|
-
const actor = await userFactory.create({ name: 'actor' })
|
|
139
|
-
const target = await userFactory.create({ name: 'target' })
|
|
140
|
-
|
|
141
|
-
const entry = await addDomainOwnerLogic(ds.manager, domain, target, actor, {
|
|
142
|
-
isMember: true
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
expect(entry.id).toBeDefined()
|
|
146
|
-
|
|
147
|
-
const reloaded = await ds.manager.findOne<DomainOwner>('DomainOwner', {
|
|
148
|
-
where: { id: entry.id },
|
|
149
|
-
relations: ['user', 'grantedBy']
|
|
150
|
-
})
|
|
151
|
-
expect(reloaded?.user.id).toBe(target.id)
|
|
152
|
-
expect(reloaded?.grantedBy?.id).toBe(actor.id)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('Domain.owner 캐시가 비어있으면 추가된 user로 세팅한다', async () => {
|
|
156
|
-
const ds = testDb.getDataSource()
|
|
157
|
-
const domain = await domainFactory.create({ owner: null as any })
|
|
158
|
-
const actor = await userFactory.create({ name: 'actor' })
|
|
159
|
-
const target = await userFactory.create({ name: 'target' })
|
|
160
|
-
|
|
161
|
-
await addDomainOwnerLogic(ds.manager, domain, target, actor, {
|
|
162
|
-
isMember: true
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
const reloadedDomain = await ds.manager.findOne<Domain>('Domain', {
|
|
166
|
-
where: { id: domain.id }
|
|
167
|
-
})
|
|
168
|
-
expect(reloadedDomain?.owner).toBe(target.id)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('Domain.owner 캐시가 이미 있으면 덮어쓰지 않는다', async () => {
|
|
172
|
-
const ds = testDb.getDataSource()
|
|
173
|
-
const existingOwner = await userFactory.create({ name: 'existing' })
|
|
174
|
-
const domain = await domainFactory.create({ owner: existingOwner.id })
|
|
175
|
-
const actor = await userFactory.create({ name: 'actor' })
|
|
176
|
-
const target = await userFactory.create({ name: 'target' })
|
|
177
|
-
|
|
178
|
-
await addDomainOwnerLogic(ds.manager, domain, target, actor, {
|
|
179
|
-
isMember: true
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
const reloadedDomain = await ds.manager.findOne<Domain>('Domain', {
|
|
183
|
-
where: { id: domain.id }
|
|
184
|
-
})
|
|
185
|
-
expect(reloadedDomain?.owner).toBe(existingOwner.id)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('도메인 비멤버는 owner로 추가할 수 없다', async () => {
|
|
189
|
-
const ds = testDb.getDataSource()
|
|
190
|
-
const domain = await domainFactory.create({})
|
|
191
|
-
const actor = await userFactory.create({ name: 'actor' })
|
|
192
|
-
const outsider = await userFactory.create({ name: 'outsider' })
|
|
193
|
-
|
|
194
|
-
await expect(
|
|
195
|
-
addDomainOwnerLogic(ds.manager, domain, outsider, actor, { isMember: false })
|
|
196
|
-
).rejects.toThrow(/not a member/)
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('이미 owner인 user를 중복 추가할 수 없다', async () => {
|
|
200
|
-
const ds = testDb.getDataSource()
|
|
201
|
-
const domain = await domainFactory.create({})
|
|
202
|
-
const actor = await userFactory.create({ name: 'actor' })
|
|
203
|
-
const target = await userFactory.create({ name: 'target' })
|
|
204
|
-
|
|
205
|
-
await addDomainOwnerLogic(ds.manager, domain, target, actor, { isMember: true })
|
|
206
|
-
|
|
207
|
-
await expect(
|
|
208
|
-
addDomainOwnerLogic(ds.manager, domain, target, actor, { isMember: true })
|
|
209
|
-
).rejects.toThrow(/already an owner/)
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
describe('removeDomainOwner 로직', () => {
|
|
214
|
-
let testDb: TestDatabase
|
|
215
|
-
|
|
216
|
-
beforeAll(() => {
|
|
217
|
-
testDb = TestDatabase.getInstance()
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('여러 owner 중 한 명을 제거할 수 있다', async () => {
|
|
221
|
-
const ds = testDb.getDataSource()
|
|
222
|
-
const domain = await domainFactory.create({})
|
|
223
|
-
const u1 = await userFactory.create({ name: 'U1' })
|
|
224
|
-
const u2 = await userFactory.create({ name: 'U2' })
|
|
225
|
-
await domainOwnerFactory.grant(domain, u1)
|
|
226
|
-
await domainOwnerFactory.grant(domain, u2)
|
|
227
|
-
|
|
228
|
-
const result = await removeDomainOwnerLogic(ds.manager, domain, u2)
|
|
229
|
-
expect(result).toBe(true)
|
|
230
|
-
|
|
231
|
-
const remaining = await ds.manager.count('DomainOwner', {
|
|
232
|
-
where: { domain: { id: domain.id } }
|
|
233
|
-
})
|
|
234
|
-
expect(remaining).toBe(1)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('마지막 남은 owner는 제거할 수 없다 (최소 1명 유지)', async () => {
|
|
238
|
-
const ds = testDb.getDataSource()
|
|
239
|
-
const domain = await domainFactory.create({})
|
|
240
|
-
const soleOwner = await userFactory.create({ name: 'sole' })
|
|
241
|
-
await domainOwnerFactory.grant(domain, soleOwner)
|
|
242
|
-
|
|
243
|
-
await expect(
|
|
244
|
-
removeDomainOwnerLogic(ds.manager, domain, soleOwner)
|
|
245
|
-
).rejects.toThrow(/last owner/)
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('legacy cache 만 있고 join 테이블에 없어도 "owner"로 인정되어 제거 거부 (마지막 1인)', async () => {
|
|
249
|
-
const ds = testDb.getDataSource()
|
|
250
|
-
const legacyOwner = await userFactory.create({ name: 'legacy' })
|
|
251
|
-
const domain = await domainFactory.create({ owner: legacyOwner.id })
|
|
252
|
-
// join table 에 엔트리 없음
|
|
253
|
-
|
|
254
|
-
await expect(
|
|
255
|
-
removeDomainOwnerLogic(ds.manager, domain, legacyOwner)
|
|
256
|
-
).rejects.toThrow(/last owner/)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('legacy cache + 다른 오너가 join 테이블에 있으면 legacy 제거 가능', async () => {
|
|
260
|
-
const ds = testDb.getDataSource()
|
|
261
|
-
const legacyOwner = await userFactory.create({ name: 'legacy' })
|
|
262
|
-
const newOwner = await userFactory.create({ name: 'new' })
|
|
263
|
-
const domain = await domainFactory.create({ owner: legacyOwner.id })
|
|
264
|
-
await domainOwnerFactory.grant(domain, newOwner)
|
|
265
|
-
|
|
266
|
-
await removeDomainOwnerLogic(ds.manager, domain, legacyOwner)
|
|
267
|
-
|
|
268
|
-
const reloadedDomain = await ds.manager.findOne<Domain>('Domain', {
|
|
269
|
-
where: { id: domain.id }
|
|
270
|
-
})
|
|
271
|
-
// legacy cache 는 남은 오너(newOwner)로 재설정되어야 함
|
|
272
|
-
expect(reloadedDomain?.owner).toBe(newOwner.id)
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it('legacy cache 대상 제거 시 남은 DomainOwner 중 한 명으로 cache 재설정', async () => {
|
|
276
|
-
const ds = testDb.getDataSource()
|
|
277
|
-
const u1 = await userFactory.create({ name: 'U1' })
|
|
278
|
-
const u2 = await userFactory.create({ name: 'U2' })
|
|
279
|
-
const u3 = await userFactory.create({ name: 'U3' })
|
|
280
|
-
const domain = await domainFactory.create({ owner: u1.id })
|
|
281
|
-
await domainOwnerFactory.grant(domain, u1)
|
|
282
|
-
await domainOwnerFactory.grant(domain, u2)
|
|
283
|
-
await domainOwnerFactory.grant(domain, u3)
|
|
284
|
-
|
|
285
|
-
await removeDomainOwnerLogic(ds.manager, domain, u1)
|
|
286
|
-
|
|
287
|
-
const reloadedDomain = await ds.manager.findOne<Domain>('Domain', {
|
|
288
|
-
where: { id: domain.id }
|
|
289
|
-
})
|
|
290
|
-
// SQLite datetime 은 초 단위 해상도라 grantedAt 이 동일할 수 있음 → 순서 비결정적.
|
|
291
|
-
// 핵심 불변조건: cache 는 u1 이 아니고, 남은 오너 (u2 또는 u3) 중 하나로 갱신됨.
|
|
292
|
-
expect(reloadedDomain?.owner).not.toBe(u1.id)
|
|
293
|
-
expect([u2.id, u3.id]).toContain(reloadedDomain?.owner)
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
it('owner가 아닌 user는 제거할 수 없다', async () => {
|
|
297
|
-
const ds = testDb.getDataSource()
|
|
298
|
-
const domain = await domainFactory.create({})
|
|
299
|
-
const owner = await userFactory.create({ name: 'owner' })
|
|
300
|
-
const other = await userFactory.create({ name: 'other' })
|
|
301
|
-
await domainOwnerFactory.grant(domain, owner)
|
|
302
|
-
|
|
303
|
-
await expect(
|
|
304
|
-
removeDomainOwnerLogic(ds.manager, domain, other)
|
|
305
|
-
).rejects.toThrow(/not an owner/)
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
it('legacy cache 만 있는 마지막 오너 + 새 오너 추가 후에는 legacy 제거 가능', async () => {
|
|
309
|
-
const ds = testDb.getDataSource()
|
|
310
|
-
const legacy = await userFactory.create({ name: 'legacy' })
|
|
311
|
-
const domain = await domainFactory.create({ owner: legacy.id })
|
|
312
|
-
// 이 시점에서 legacy는 cache로만 유일한 owner → 제거 불가
|
|
313
|
-
await expect(
|
|
314
|
-
removeDomainOwnerLogic(ds.manager, domain, legacy)
|
|
315
|
-
).rejects.toThrow(/last owner/)
|
|
316
|
-
|
|
317
|
-
// 새 오너를 추가하면
|
|
318
|
-
const newOwner = await userFactory.create({ name: 'new' })
|
|
319
|
-
const actor = await userFactory.create({ name: 'actor' })
|
|
320
|
-
await addDomainOwnerLogic(ds.manager, domain, newOwner, actor, { isMember: true })
|
|
321
|
-
|
|
322
|
-
// legacy를 제거할 수 있음 (remaining = legacy 1 + new 1 - 1 = 1)
|
|
323
|
-
// NOTE: addDomainOwnerLogic은 domain.owner가 이미 legacy.id 이므로 덮어쓰지 않음
|
|
324
|
-
// 따라서 domain 은 여전히 legacy.id 가 cache
|
|
325
|
-
const reloadedDomain = await ds.manager.findOne<Domain>('Domain', {
|
|
326
|
-
where: { id: domain.id }
|
|
327
|
-
})
|
|
328
|
-
expect(reloadedDomain!.owner).toBe(legacy.id)
|
|
329
|
-
|
|
330
|
-
await removeDomainOwnerLogic(ds.manager, reloadedDomain!, legacy)
|
|
331
|
-
|
|
332
|
-
const finalDomain = await ds.manager.findOne<Domain>('Domain', {
|
|
333
|
-
where: { id: domain.id }
|
|
334
|
-
})
|
|
335
|
-
expect(finalDomain?.owner).toBe(newOwner.id)
|
|
336
|
-
})
|
|
337
|
-
})
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 도메인 소유권 판정 — 실제 엔티티·실제 쿼리 검증.
|
|
3
|
-
*
|
|
4
|
-
* 도메인 관리자는 여러 명일 수 있다(`DomainOwner` 테이블). `Domain.owner` 컬럼은 대표 오너
|
|
5
|
-
* 표시용 캐시이므로, 도메인 범위의 판정은 두 경로를 함께 봐야 한다.
|
|
6
|
-
*
|
|
7
|
-
* 주의: 이 스펙은 빌드 산출물(`dist-server`)을 대상으로 한다.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { DataSource } from 'typeorm'
|
|
11
|
-
|
|
12
|
-
import { NamingStrategy, addDataSource, entities as shellEntities, Domain } from '@things-factory/shell'
|
|
13
|
-
import {
|
|
14
|
-
DomainOwner,
|
|
15
|
-
User,
|
|
16
|
-
entities as authEntities,
|
|
17
|
-
findDomainOwnerUserIds,
|
|
18
|
-
isDomainOwner,
|
|
19
|
-
setPrimaryDomainOwner
|
|
20
|
-
} from '@things-factory/auth-base'
|
|
21
|
-
|
|
22
|
-
let dataSource: DataSource
|
|
23
|
-
|
|
24
|
-
async function domain(name: string): Promise<Domain> {
|
|
25
|
-
return await dataSource.getRepository(Domain).save({ name, subdomain: name } as any)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function usr(name: string): Promise<User> {
|
|
29
|
-
return await dataSource.getRepository(User).save({
|
|
30
|
-
name,
|
|
31
|
-
email: `${name}@test.com`,
|
|
32
|
-
username: name
|
|
33
|
-
} as any)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function reload(target: Domain): Promise<Domain> {
|
|
37
|
-
return await dataSource.getRepository(Domain).findOneBy({ id: target.id })
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function ownerEntryCount(target: Domain): Promise<number> {
|
|
41
|
-
return await dataSource.getRepository(DomainOwner).count({ where: { domain: { id: target.id } } })
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
beforeAll(async () => {
|
|
45
|
-
dataSource = new DataSource({
|
|
46
|
-
type: 'sqlite',
|
|
47
|
-
database: ':memory:',
|
|
48
|
-
synchronize: true,
|
|
49
|
-
dropSchema: true,
|
|
50
|
-
namingStrategy: new NamingStrategy(),
|
|
51
|
-
entities: [...shellEntities, ...authEntities],
|
|
52
|
-
logging: false
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
await dataSource.initialize()
|
|
56
|
-
addDataSource('default', dataSource)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
afterAll(async () => {
|
|
60
|
-
await dataSource?.destroy()
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
beforeEach(async () => {
|
|
64
|
-
for (const junction of dataSource.entityMetadatas.filter(metadata => metadata.isJunction)) {
|
|
65
|
-
await dataSource.query(`DELETE FROM "${junction.tableName}"`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const target of [DomainOwner, User, Domain]) {
|
|
69
|
-
await dataSource.query(`DELETE FROM "${dataSource.getMetadata(target).tableName}"`)
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('isDomainOwner', () => {
|
|
74
|
-
it('대표 오너 컬럼으로 인정한다', async () => {
|
|
75
|
-
const target = await domain('acme')
|
|
76
|
-
const owner = await usr('boss')
|
|
77
|
-
await dataSource.getRepository(Domain).update(target.id, { owner: owner.id })
|
|
78
|
-
|
|
79
|
-
expect(await isDomainOwner(await reload(target), owner)).toBe(true)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('DomainOwner 테이블의 공동 오너도 인정한다', async () => {
|
|
83
|
-
const target = await domain('acme')
|
|
84
|
-
const primary = await usr('boss')
|
|
85
|
-
const coOwner = await usr('deputy')
|
|
86
|
-
await dataSource.getRepository(Domain).update(target.id, { owner: primary.id })
|
|
87
|
-
await dataSource.getRepository(DomainOwner).save({ domain: target, user: coOwner } as any)
|
|
88
|
-
|
|
89
|
-
expect(await isDomainOwner(await reload(target), coOwner)).toBe(true)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('오너가 아닌 사용자는 인정하지 않는다', async () => {
|
|
93
|
-
const target = await domain('acme')
|
|
94
|
-
const owner = await usr('boss')
|
|
95
|
-
const other = await usr('lee')
|
|
96
|
-
await dataSource.getRepository(Domain).update(target.id, { owner: owner.id })
|
|
97
|
-
|
|
98
|
-
expect(await isDomainOwner(await reload(target), other)).toBe(false)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('다른 도메인의 오너 엔트리는 인정하지 않는다', async () => {
|
|
102
|
-
const target = await domain('acme')
|
|
103
|
-
const another = await domain('other')
|
|
104
|
-
const coOwner = await usr('deputy')
|
|
105
|
-
await dataSource.getRepository(DomainOwner).save({ domain: another, user: coOwner } as any)
|
|
106
|
-
|
|
107
|
-
expect(await isDomainOwner(await reload(target), coOwner)).toBe(false)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('도메인이나 사용자가 없으면 인정하지 않는다', async () => {
|
|
111
|
-
const target = await domain('acme')
|
|
112
|
-
const owner = await usr('boss')
|
|
113
|
-
|
|
114
|
-
expect(await isDomainOwner(null, owner)).toBe(false)
|
|
115
|
-
expect(await isDomainOwner(await reload(target), null)).toBe(false)
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe('findDomainOwnerUserIds', () => {
|
|
120
|
-
it('대표 오너와 공동 오너를 합쳐서 반환한다', async () => {
|
|
121
|
-
const target = await domain('acme')
|
|
122
|
-
const primary = await usr('boss')
|
|
123
|
-
const coOwner = await usr('deputy')
|
|
124
|
-
const plain = await usr('lee')
|
|
125
|
-
await dataSource.getRepository(Domain).update(target.id, { owner: primary.id })
|
|
126
|
-
await dataSource.getRepository(DomainOwner).save({ domain: target, user: coOwner } as any)
|
|
127
|
-
|
|
128
|
-
const ownerIds = await findDomainOwnerUserIds(await reload(target))
|
|
129
|
-
|
|
130
|
-
expect(ownerIds.has(primary.id)).toBe(true)
|
|
131
|
-
expect(ownerIds.has(coOwner.id)).toBe(true)
|
|
132
|
-
expect(ownerIds.has(plain.id)).toBe(false)
|
|
133
|
-
expect(ownerIds.size).toBe(2)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('대표 오너가 테이블에도 있으면 중복 계산하지 않는다', async () => {
|
|
137
|
-
const target = await domain('acme')
|
|
138
|
-
const primary = await usr('boss')
|
|
139
|
-
await dataSource.getRepository(Domain).update(target.id, { owner: primary.id })
|
|
140
|
-
await dataSource.getRepository(DomainOwner).save({ domain: target, user: primary } as any)
|
|
141
|
-
|
|
142
|
-
expect((await findDomainOwnerUserIds(await reload(target))).size).toBe(1)
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
describe('setPrimaryDomainOwner', () => {
|
|
147
|
-
it('컬럼과 테이블을 함께 세팅한다', async () => {
|
|
148
|
-
const target = await domain('acme')
|
|
149
|
-
const owner = await usr('boss')
|
|
150
|
-
|
|
151
|
-
await setPrimaryDomainOwner(target, owner.id)
|
|
152
|
-
|
|
153
|
-
expect((await reload(target)).owner).toBe(owner.id)
|
|
154
|
-
expect(await ownerEntryCount(target)).toBe(1)
|
|
155
|
-
expect(await isDomainOwner(await reload(target), owner)).toBe(true)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('기존 공동 오너 엔트리를 제거하지 않는다', async () => {
|
|
159
|
-
const target = await domain('acme')
|
|
160
|
-
const previous = await usr('boss')
|
|
161
|
-
const next = await usr('deputy')
|
|
162
|
-
await setPrimaryDomainOwner(target, previous.id)
|
|
163
|
-
|
|
164
|
-
await setPrimaryDomainOwner(target, next.id)
|
|
165
|
-
|
|
166
|
-
expect((await reload(target)).owner).toBe(next.id)
|
|
167
|
-
expect(await ownerEntryCount(target)).toBe(2)
|
|
168
|
-
/* 이관 후에도 이전 오너는 공동 오너로 남는다 — 제거는 removeDomainOwner 의 책임. */
|
|
169
|
-
expect(await isDomainOwner(await reload(target), previous)).toBe(true)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('같은 오너를 다시 지정해도 엔트리를 중복 생성하지 않는다', async () => {
|
|
173
|
-
const target = await domain('acme')
|
|
174
|
-
const owner = await usr('boss')
|
|
175
|
-
|
|
176
|
-
await setPrimaryDomainOwner(target, owner.id)
|
|
177
|
-
await setPrimaryDomainOwner(target, owner.id)
|
|
178
|
-
|
|
179
|
-
expect(await ownerEntryCount(target)).toBe(1)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('도메인이나 사용자가 없으면 명시적으로 실패한다', async () => {
|
|
183
|
-
const target = await domain('acme')
|
|
184
|
-
|
|
185
|
-
await expect(setPrimaryDomainOwner(target, '')).rejects.toThrow()
|
|
186
|
-
await expect(setPrimaryDomainOwner(null as any, 'some-user')).rejects.toThrow()
|
|
187
|
-
})
|
|
188
|
-
})
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the assistant reads to draft access decisions (ADR-0103 decision 6).
|
|
3
|
-
*
|
|
4
|
-
* Two facts are held here. The read tool answers only this domain, only to someone who may decide, never the
|
|
5
|
-
* caller's own requests. And a pending request handed over as the screen's context does not reach the model's
|
|
6
|
-
* prompt - which is why the tool exists; if that ever changes, this file says so.
|
|
7
|
-
*/
|
|
8
|
-
import { registryAssistantChat } from '../../ai-client-base/server/service/assistant/registry-assistant'
|
|
9
|
-
import { clearToolRegistry, registerToolCategory } from '../../ai-client-base/server/service/tool-registry/index.js'
|
|
10
|
-
|
|
11
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
12
|
-
import { loadCompiled } from './compiled'
|
|
13
|
-
|
|
14
|
-
import uiEn from '../../auth-ui/translations/en.json'
|
|
15
|
-
import uiKo from '../../auth-ui/translations/ko.json'
|
|
16
|
-
|
|
17
|
-
const { Role } = loadCompiled('service/role/role')
|
|
18
|
-
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
19
|
-
const { User } = loadCompiled('service/user/user')
|
|
20
|
-
const { DomainOwner } = loadCompiled('service/domain-owner/domain-owner')
|
|
21
|
-
const { AccessRequest } = loadCompiled('service/access-request/access-request')
|
|
22
|
-
const access = loadCompiled('controllers/access-request')
|
|
23
|
-
const assistant = loadCompiled('ai-entry/access-assistant')
|
|
24
|
-
const targets = loadCompiled('ai-entry/access-entry-targets')
|
|
25
|
-
|
|
26
|
-
let domain: any
|
|
27
|
-
let other: any
|
|
28
|
-
let kim: any
|
|
29
|
-
let lee: any
|
|
30
|
-
let park: any
|
|
31
|
-
let owner: any
|
|
32
|
-
let worker: any
|
|
33
|
-
let otherAdmin: any
|
|
34
|
-
|
|
35
|
-
async function makeUser(name: string, domains: any[], roles: any[] = []) {
|
|
36
|
-
return await getRepository(User).save({
|
|
37
|
-
username: name, email: `${name}@example.com`, name, salt: User.generateSalt(), status: 'activated', domains, roles
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const ask = (user: any, privilege = 'mutation', inDomain = domain) =>
|
|
42
|
-
access.requestAccess({ category: 'warehouse', privilege, reason: 'needed for work' }, { state: { domain: inDomain, user, afterCommit: [] } } as any)
|
|
43
|
-
|
|
44
|
-
beforeAll(async () => {
|
|
45
|
-
await openAuthDatabase()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
afterAll(async () => {
|
|
49
|
-
await closeAuthDatabase()
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
beforeEach(async () => {
|
|
53
|
-
await resetAuthDatabase()
|
|
54
|
-
;(process as any).domainOwnerGranted = async (d: any, u: any) =>
|
|
55
|
-
!!(await getRepository(DomainOwner).findOne({ where: { domain: { id: d.id }, user: { id: u.id } } }))
|
|
56
|
-
;(process as any).superUserGranted = async () => false
|
|
57
|
-
|
|
58
|
-
domain = await getRepository(Domain).findOneBy({ id: (await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })).id })
|
|
59
|
-
other = await getRepository(Domain).findOneBy({ id: (await getRepository(Domain).save({ name: 'Other', subdomain: 'other' })).id })
|
|
60
|
-
|
|
61
|
-
const mutation = await getRepository(Privilege).save({ name: 'mutation', category: 'warehouse' })
|
|
62
|
-
await getRepository(Privilege).save({ name: 'query', category: 'warehouse' })
|
|
63
|
-
const grant = await getRepository(Privilege).save({ name: 'mutation', category: 'user' })
|
|
64
|
-
await getRepository(Role).save({ name: 'inbound', domain, privileges: [mutation] })
|
|
65
|
-
const admins = await getRepository(Role).save({ name: 'admins', domain, privileges: [grant] })
|
|
66
|
-
const otherAdmins = await getRepository(Role).save({ name: 'admins', domain: other, privileges: [grant] })
|
|
67
|
-
|
|
68
|
-
kim = await makeUser('kim', [domain])
|
|
69
|
-
lee = await makeUser('lee', [domain])
|
|
70
|
-
park = await makeUser('park', [domain], [admins])
|
|
71
|
-
owner = await makeUser('owner', [domain])
|
|
72
|
-
worker = await makeUser('worker', [domain])
|
|
73
|
-
otherAdmin = await makeUser('otheradmin', [other], [otherAdmins])
|
|
74
|
-
await getRepository(DomainOwner).save({ domain, user: owner })
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
delete (process as any).domainOwnerGranted
|
|
79
|
-
delete (process as any).superUserGranted
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
describe('listPendingAccessRequests', () => {
|
|
83
|
-
it('★ a decider reads this domain\'s waiting requests with the roles that grant each', async () => {
|
|
84
|
-
await ask(kim)
|
|
85
|
-
const answer = await assistant.pendingRequestsToDecide({ domain, user: park })
|
|
86
|
-
expect(answer.requests).toEqual([
|
|
87
|
-
expect.objectContaining({ requester: 'kim', privilege: 'warehouse:mutation', task: null, roles: [{ name: 'inbound', privilegeCount: 1, holdsTask: false }] })
|
|
88
|
-
])
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('★ another domain\'s requests are not read - even by a decider there', async () => {
|
|
92
|
-
await ask(kim)
|
|
93
|
-
const there = await assistant.pendingRequestsToDecide({ domain: other, user: otherAdmin })
|
|
94
|
-
expect(there.requests).toEqual([])
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('★ someone who may not decide is refused - nothing is read', async () => {
|
|
98
|
-
await ask(kim)
|
|
99
|
-
expect(await assistant.pendingRequestsToDecide({ domain, user: worker })).toEqual({ refused: 'not-a-decider' })
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('★ the caller\'s own requests are left out - they cannot decide them', async () => {
|
|
103
|
-
await ask(kim)
|
|
104
|
-
await ask(owner, 'query')
|
|
105
|
-
const answer = await assistant.pendingRequestsToDecide({ domain, user: owner })
|
|
106
|
-
expect(answer.requests.map((r: any) => r.requester)).toEqual(['kim'])
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('decided and expired requests are left out', async () => {
|
|
110
|
-
const decided = await ask(kim)
|
|
111
|
-
await access.decideAccessRequest({ requestId: decided.id, decision: 'DENY', reason: 'no' }, { state: { domain, user: park, afterCommit: [] } } as any)
|
|
112
|
-
const late = await ask(lee)
|
|
113
|
-
await getRepository(AccessRequest).update({ id: late.id }, { expiresAt: new Date(Date.now() - 1000) })
|
|
114
|
-
expect((await assistant.pendingRequestsToDecide({ domain, user: park })).requests).toEqual([])
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
describe('the declarations fit together', () => {
|
|
119
|
-
afterEach(() => clearToolRegistry())
|
|
120
|
-
|
|
121
|
-
it('the category registers, the mode names it and the decision target, the words exist', () => {
|
|
122
|
-
const category = assistant.accessToolCategory()
|
|
123
|
-
expect(() => registerToolCategory(category)).not.toThrow()
|
|
124
|
-
const mode = assistant.accessAssistantMode()
|
|
125
|
-
expect(mode.toolCategories).toEqual([category.name])
|
|
126
|
-
expect(mode.entryTargets).toEqual([targets.AUTH_ACCESS_DECISION])
|
|
127
|
-
expect(mode.subjects).toEqual([assistant.ACCESS_REQUESTS_SUBJECT])
|
|
128
|
-
for (const dict of [uiEn, uiKo] as Record<string, string>[])
|
|
129
|
-
for (const key of [mode.labelKey, mode.placeholderKey]) expect([key, typeof dict[key]]).toEqual([key, 'string'])
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('★ requests handed over as the screen\'s context do not reach the model\'s prompt - the tool is how it reads them', async () => {
|
|
133
|
-
registerToolCategory(assistant.accessToolCategory())
|
|
134
|
-
const seen: any[] = []
|
|
135
|
-
const client = {
|
|
136
|
-
chatWithTools: async (messages: any, tools: any[], options: any) => {
|
|
137
|
-
seen.push({ messages, tools, options })
|
|
138
|
-
return { text: 'ok', toolCalls: [], stopReason: 'end_turn' }
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
await registryAssistantChat({
|
|
142
|
-
client,
|
|
143
|
-
messages: [{ role: 'user', content: 'sort out the access requests' }],
|
|
144
|
-
systemPrompt: assistant.accessAssistantMode().systemPrompt,
|
|
145
|
-
toolCategories: [assistant.AUTH_ACCESS_TOOLS],
|
|
146
|
-
hostContext: { pendingRequests: ['MARKER-kim-warehouse-mutation'] },
|
|
147
|
-
state: { domain, user: park },
|
|
148
|
-
toolAccess: async () => true
|
|
149
|
-
} as any)
|
|
150
|
-
|
|
151
|
-
expect(JSON.stringify(seen[0].options)).not.toContain('MARKER-kim')
|
|
152
|
-
expect(JSON.stringify(seen[0].messages)).not.toContain('MARKER-kim')
|
|
153
|
-
expect(seen[0].tools.map((t: any) => t.name)).toEqual(['listPendingAccessRequests'])
|
|
154
|
-
})
|
|
155
|
-
})
|