@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,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `User.hasPrivilege` — **질의**를 실 DB 로 태운다.
|
|
3
|
-
*
|
|
4
|
-
* 이 함수가 이름 있는 권한 판정의 마지막 자리다(`checkPermission` 이 소유권 우회를 지나면 여기로
|
|
5
|
-
* 온다). `permission-gate.test.ts` 는 그 앞의 규칙만 보고 이 함수는 목으로 대신했다 — 즉
|
|
6
|
-
* **역할·권한·도메인 상속이 실제로 어떻게 이어지는지는 시험된 적이 없었다.**
|
|
7
|
-
*
|
|
8
|
-
* 열린 방 509개를 닫는 일이 이 질의 위에 선다. 지시자를 다 달아 놓고 질의가 틀리면, 닫은 것이
|
|
9
|
-
* 아니라 **아무도 못 들어오게 만든 것**이 된다.
|
|
10
|
-
*
|
|
11
|
-
* ── 상속 규약 ───────────────────────────────────────────────────────────────
|
|
12
|
-
* 역할은 도메인에 정의되고 자식 도메인이 **부모 1단**을 상속한다
|
|
13
|
-
* (`applyDomainInheritanceFilter` 가 단일 소스: `domain IN (자기, 부모)`).
|
|
14
|
-
* 그래서 판정은 「어느 도메인에 정의된 역할인가」를 보고, 「어느 도메인에서 부여받았나」는
|
|
15
|
-
* 체크인 게이트(`checkin-domain-rule`)가 본다 — 그쪽은 순수 시험이 따로 있다.
|
|
16
|
-
*
|
|
17
|
-
* ⚠ `parentId` 는 `@RelationId` 라 **저장소로 읽은 도메인에만** 실린다. 손으로 만든 객체를
|
|
18
|
-
* 넘기면 상속 경로가 조용히 성립하지 않는다. 그래서 아래는 전부 다시 읽어서 넘긴다.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
22
|
-
import { loadCompiled } from './compiled'
|
|
23
|
-
|
|
24
|
-
const { User } = loadCompiled('service/user/user')
|
|
25
|
-
const { Role } = loadCompiled('service/role/role')
|
|
26
|
-
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
27
|
-
|
|
28
|
-
let parent: any
|
|
29
|
-
let child: any
|
|
30
|
-
let sibling: any
|
|
31
|
-
let grandchild: any
|
|
32
|
-
let boardMutation: any
|
|
33
|
-
|
|
34
|
-
/** `parentId` 가 실리도록 저장소로 다시 읽는다. */
|
|
35
|
-
async function loadDomain(id: string) {
|
|
36
|
-
return await getRepository(Domain).findOneBy({ id })
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function makeRole(name: string, domain: any, privileges: any[]) {
|
|
40
|
-
return await getRepository(Role).save({ name, domain, privileges })
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function makeUser(memberOf: any[], roles: any[]) {
|
|
44
|
-
const salt = User.generateSalt()
|
|
45
|
-
|
|
46
|
-
return await getRepository(User).save({
|
|
47
|
-
username: `kim-${Math.random().toString(36).slice(2, 8)}`,
|
|
48
|
-
email: `kim-${Math.random().toString(36).slice(2, 8)}@example.com`,
|
|
49
|
-
name: 'Kim',
|
|
50
|
-
salt,
|
|
51
|
-
status: 'activated',
|
|
52
|
-
domains: memberOf,
|
|
53
|
-
roles
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
beforeAll(async () => {
|
|
58
|
-
await openAuthDatabase()
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
beforeEach(async () => {
|
|
62
|
-
await resetAuthDatabase()
|
|
63
|
-
|
|
64
|
-
const p = await getRepository(Domain).save({ name: 'group', subdomain: 'group' })
|
|
65
|
-
parent = await loadDomain(p.id)
|
|
66
|
-
|
|
67
|
-
const c = await getRepository(Domain).save({ name: 'plant-a', subdomain: 'plant-a', parent })
|
|
68
|
-
child = await loadDomain(c.id)
|
|
69
|
-
|
|
70
|
-
const s = await getRepository(Domain).save({ name: 'plant-b', subdomain: 'plant-b', parent })
|
|
71
|
-
sibling = await loadDomain(s.id)
|
|
72
|
-
|
|
73
|
-
const g = await getRepository(Domain).save({ name: 'line-1', subdomain: 'line-1', parent: child })
|
|
74
|
-
grandchild = await loadDomain(g.id)
|
|
75
|
-
|
|
76
|
-
boardMutation = await getRepository(Privilege).save({ name: 'mutation', category: 'board' })
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
afterAll(async () => {
|
|
80
|
-
await closeAuthDatabase()
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
describe('자기 도메인의 역할', () => {
|
|
84
|
-
it('그 도메인에서 통과한다', async () => {
|
|
85
|
-
const role = await makeRole('editor', child, [boardMutation])
|
|
86
|
-
const user = await makeUser([child], [role])
|
|
87
|
-
|
|
88
|
-
expect(await User.hasPrivilege('mutation', 'board', child, user)).toBe(true)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('권한이 다르면 통과하지 못한다', async () => {
|
|
92
|
-
const role = await makeRole('editor', child, [boardMutation])
|
|
93
|
-
const user = await makeUser([child], [role])
|
|
94
|
-
|
|
95
|
-
expect(await User.hasPrivilege('query', 'board', child, user)).toBe(false)
|
|
96
|
-
expect(await User.hasPrivilege('mutation', 'twin', child, user)).toBe(false)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('역할이 없으면 통과하지 못한다', async () => {
|
|
100
|
-
const user = await makeUser([child], [])
|
|
101
|
-
|
|
102
|
-
expect(await User.hasPrivilege('mutation', 'board', child, user)).toBe(false)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('역할은 있는데 권한이 안 붙어 있으면 통과하지 못한다', async () => {
|
|
106
|
-
const role = await makeRole('viewer', child, [])
|
|
107
|
-
const user = await makeUser([child], [role])
|
|
108
|
-
|
|
109
|
-
expect(await User.hasPrivilege('mutation', 'board', child, user)).toBe(false)
|
|
110
|
-
})
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
describe('부모 도메인의 역할 — 1단 상속', () => {
|
|
114
|
-
it('부모에 정의된 역할이 자식 도메인에서 통과한다', async () => {
|
|
115
|
-
const role = await makeRole('group-editor', parent, [boardMutation])
|
|
116
|
-
const user = await makeUser([child], [role])
|
|
117
|
-
|
|
118
|
-
expect(await User.hasPrivilege('mutation', 'board', child, user)).toBe(true)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('할아버지 도메인의 역할은 손자에서 통과하지 못한다 — 상속은 1단이다', async () => {
|
|
122
|
-
const role = await makeRole('group-editor', parent, [boardMutation])
|
|
123
|
-
const user = await makeUser([grandchild], [role])
|
|
124
|
-
|
|
125
|
-
expect(await User.hasPrivilege('mutation', 'board', grandchild, user)).toBe(false)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('형제 도메인의 역할은 통과하지 못한다', async () => {
|
|
129
|
-
const role = await makeRole('b-editor', sibling, [boardMutation])
|
|
130
|
-
const user = await makeUser([child], [role])
|
|
131
|
-
|
|
132
|
-
expect(await User.hasPrivilege('mutation', 'board', child, user)).toBe(false)
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
describe('⚠ 이 질의가 멤버십을 보지 않는다', () => {
|
|
137
|
-
it('그 도메인의 멤버가 아니어도 역할만 있으면 통과한다', async () => {
|
|
138
|
-
/*
|
|
139
|
-
* 결함이 아니다 — 나누어 맡긴 것이다. 멤버십은 체크인 게이트가 강제하고
|
|
140
|
-
* (`checkin-domain-rule` 이 `memberDomainIds` 를 AND 조건으로 건다), 여기까지 온 요청은
|
|
141
|
-
* 이미 그 문을 지났다.
|
|
142
|
-
*
|
|
143
|
-
* 다만 **이 함수를 그 문 밖에서 부르면 격리가 성립하지 않는다.** 미들웨어를 타지 않는
|
|
144
|
-
* 경로(주기 작업·내부 호출)에서 쓸 때 그 사실을 알고 있어야 한다.
|
|
145
|
-
*/
|
|
146
|
-
const role = await makeRole('editor', child, [boardMutation])
|
|
147
|
-
const user = await makeUser([sibling], [role])
|
|
148
|
-
|
|
149
|
-
expect(await User.hasPrivilege('mutation', 'board', child, user)).toBe(true)
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
describe('parentId 가 안 실린 도메인 객체', () => {
|
|
154
|
-
it('손으로 만든 객체를 넘기면 상속이 성립하지 않는다', async () => {
|
|
155
|
-
/*
|
|
156
|
-
* `parentId` 는 `@RelationId` 라 저장소로 읽은 엔티티에만 실린다. 그래서 `{ id }` 만 담은
|
|
157
|
-
* 객체를 넘기면 상속 경로가 **조용히** 사라진다 — 부모 역할로 들어오던 사람이 거절된다.
|
|
158
|
-
* 부르는 쪽이 온전히 적재된 도메인을 넘겨야 한다는 뜻이고, 그것을 시험으로 못 박아 둔다.
|
|
159
|
-
*/
|
|
160
|
-
const role = await makeRole('group-editor', parent, [boardMutation])
|
|
161
|
-
const user = await makeUser([child], [role])
|
|
162
|
-
|
|
163
|
-
expect(await User.hasPrivilege('mutation', 'board', child, user)).toBe(true)
|
|
164
|
-
expect(await User.hasPrivilege('mutation', 'board', { id: child.id } as any, user)).toBe(false)
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
describe('getPrivilegesByDomain', () => {
|
|
169
|
-
it('자기 도메인과 부모 도메인의 권한을 함께 돌려준다', async () => {
|
|
170
|
-
const boardQuery = await getRepository(Privilege).save({ name: 'query', category: 'board' })
|
|
171
|
-
const twinMutation = await getRepository(Privilege).save({ name: 'mutation', category: 'twin' })
|
|
172
|
-
|
|
173
|
-
const parentRole = await makeRole('group-editor', parent, [twinMutation])
|
|
174
|
-
const childRole = await makeRole('editor', child, [boardMutation, boardQuery])
|
|
175
|
-
const user = await makeUser([child], [parentRole, childRole])
|
|
176
|
-
|
|
177
|
-
const granted = await User.getPrivilegesByDomain(user, child)
|
|
178
|
-
|
|
179
|
-
expect(granted).toEqual(
|
|
180
|
-
expect.arrayContaining([
|
|
181
|
-
{ category: 'board', privilege: 'mutation' },
|
|
182
|
-
{ category: 'board', privilege: 'query' },
|
|
183
|
-
{ category: 'twin', privilege: 'mutation' }
|
|
184
|
-
])
|
|
185
|
-
)
|
|
186
|
-
expect(granted).toHaveLength(3)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('같은 권한을 두 역할로 받아도 한 번만 돌려준다', async () => {
|
|
190
|
-
const one = await makeRole('editor-1', child, [boardMutation])
|
|
191
|
-
const two = await makeRole('editor-2', child, [boardMutation])
|
|
192
|
-
const user = await makeUser([child], [one, two])
|
|
193
|
-
|
|
194
|
-
const granted = await User.getPrivilegesByDomain(user, child)
|
|
195
|
-
|
|
196
|
-
expect(granted).toEqual([{ category: 'board', privilege: 'mutation' }])
|
|
197
|
-
})
|
|
198
|
-
})
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@recentConfirmation` (ADR-0168) - the seam. A door that asks for a recent confirmation opens only when the
|
|
3
|
-
* person confirmed in this browser within the last ten minutes, as themself; passing does not lengthen it. Built as SDL the way
|
|
4
|
-
* `privilege-directive.test.ts` builds its schema - the wrapper sees only the field, `context.state` and the cookies.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { graphql } from 'graphql'
|
|
8
|
-
import { makeExecutableSchema } from '@graphql-tools/schema'
|
|
9
|
-
import jwt from 'jsonwebtoken'
|
|
10
|
-
|
|
11
|
-
import { loadCompiled } from './compiled'
|
|
12
|
-
|
|
13
|
-
const { privilegeDirectiveTypeDefs, privilegeDirectiveResolver } = loadCompiled('service/privilege/privilege-directive')
|
|
14
|
-
const confirmation = loadCompiled('service/identity-confirmation/identity-confirmation')
|
|
15
|
-
const { SECRET } = loadCompiled('utils/get-secret')
|
|
16
|
-
const { User } = loadCompiled('service/user/user')
|
|
17
|
-
|
|
18
|
-
const DOMAIN = { id: 'domain-1', subdomain: 'office' }
|
|
19
|
-
const KIM = { id: 'kim', domains: [DOMAIN] }
|
|
20
|
-
const LEE = { id: 'lee', domains: [DOMAIN] }
|
|
21
|
-
|
|
22
|
-
const typeDefs = `
|
|
23
|
-
type Query {
|
|
24
|
-
myPayslips: String @privilege(category: "office-self", privilege: "query") @recentConfirmation
|
|
25
|
-
myLeave: String @privilege(category: "office-self", privilege: "query")
|
|
26
|
-
}
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
function buildSchema(defs = typeDefs) {
|
|
30
|
-
return privilegeDirectiveResolver(
|
|
31
|
-
makeExecutableSchema({
|
|
32
|
-
typeDefs: [privilegeDirectiveTypeDefs, confirmation.recentConfirmationDirectiveTypeDefs, defs],
|
|
33
|
-
resolvers: { Query: { myPayslips: () => 'slips', myLeave: () => 'leave' } }
|
|
34
|
-
})
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** A browser: the cookies it holds between requests. */
|
|
39
|
-
function browser() {
|
|
40
|
-
const jar = new Map<string, string>()
|
|
41
|
-
const options = new Map<string, any>()
|
|
42
|
-
return {
|
|
43
|
-
jar,
|
|
44
|
-
options,
|
|
45
|
-
cookies: {
|
|
46
|
-
get: (name: string) => jar.get(name) || undefined,
|
|
47
|
-
set: (name: string, value: string, option: any) => (jar.set(name, value), options.set(name, option))
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function run(schema: any, field: string, user: any, at: ReturnType<typeof browser>) {
|
|
53
|
-
return graphql({ schema, source: `{ ${field} }`, contextValue: { state: { domain: DOMAIN, user }, cookies: at.cookies } })
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const refusalOf = (result: any) => result.errors?.[0]?.extensions?.refusal?.code
|
|
57
|
-
|
|
58
|
-
beforeEach(() => {
|
|
59
|
-
User.hasPrivilege = jest.fn().mockResolvedValue(true)
|
|
60
|
-
;(process as any).domainOwnerGranted = jest.fn().mockResolvedValue(false)
|
|
61
|
-
;(process as any).superUserGranted = jest.fn().mockResolvedValue(false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
afterEach(() => {
|
|
65
|
-
delete (process as any).domainOwnerGranted
|
|
66
|
-
delete (process as any).superUserGranted
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('refuses a door that asks for it when the person has not confirmed, with a code the screen can name', async () => {
|
|
70
|
-
const result = await run(buildSchema(), 'myPayslips', KIM, browser())
|
|
71
|
-
|
|
72
|
-
expect(result.data?.myPayslips ?? null).toBeNull()
|
|
73
|
-
expect(refusalOf(result)).toBe('identity-confirmation-required')
|
|
74
|
-
expect(result.errors?.[0]?.extensions?.code).toBe('IDENTITY_CONFIRMATION_REQUIRED')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('opens it after the person confirmed, and does not lengthen the confirmation as it passes', async () => {
|
|
78
|
-
const at = browser()
|
|
79
|
-
confirmation.rememberConfirmation({ cookies: at.cookies }, KIM.id)
|
|
80
|
-
const given = at.jar.get('identity_confirmed')
|
|
81
|
-
|
|
82
|
-
await new Promise(resolve => setTimeout(resolve, 1100))
|
|
83
|
-
const result = await run(buildSchema(), 'myPayslips', KIM, at)
|
|
84
|
-
|
|
85
|
-
expect(result.errors).toBeUndefined()
|
|
86
|
-
expect(result.data).toEqual({ myPayslips: 'slips' })
|
|
87
|
-
expect(at.jar.get('identity_confirmed')).toBe(given)
|
|
88
|
-
expect(at.options.get('identity_confirmed')).toMatchObject({ httpOnly: true, maxAge: 10 * 60 * 1000 })
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('holds for ten minutes from the moment it was given, and says until when', () => {
|
|
92
|
-
const at = browser()
|
|
93
|
-
const before = Date.now()
|
|
94
|
-
confirmation.rememberConfirmation({ cookies: at.cookies }, KIM.id)
|
|
95
|
-
|
|
96
|
-
const until = confirmation.confirmedUntil({ cookies: at.cookies }, KIM.id).getTime()
|
|
97
|
-
expect(until).toBeGreaterThanOrEqual(Math.floor(before / 1000) * 1000 + 10 * 60 * 1000)
|
|
98
|
-
expect(until).toBeLessThanOrEqual(Date.now() + 10 * 60 * 1000)
|
|
99
|
-
expect(confirmation.confirmedUntil({ cookies: at.cookies }, LEE.id)).toBeNull()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it("does not open on another person's confirmation", async () => {
|
|
103
|
-
const at = browser()
|
|
104
|
-
confirmation.rememberConfirmation({ cookies: at.cookies }, LEE.id)
|
|
105
|
-
|
|
106
|
-
expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, at))).toBe('identity-confirmation-required')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('does not open on an expired confirmation, a forged one, or one let go', async () => {
|
|
110
|
-
const expired = browser()
|
|
111
|
-
expired.jar.set('identity_confirmed', jwt.sign({ sub: KIM.id, purpose: 'identity-confirmation' }, SECRET, { expiresIn: -10 }))
|
|
112
|
-
expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, expired))).toBe('identity-confirmation-required')
|
|
113
|
-
|
|
114
|
-
const forged = browser()
|
|
115
|
-
forged.jar.set('identity_confirmed', jwt.sign({ sub: KIM.id, purpose: 'identity-confirmation' }, 'another secret', { expiresIn: 300 }))
|
|
116
|
-
expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, forged))).toBe('identity-confirmation-required')
|
|
117
|
-
|
|
118
|
-
const signedIn = browser()
|
|
119
|
-
signedIn.jar.set('identity_confirmed', jwt.sign({ sub: KIM.id }, SECRET, { expiresIn: 300 }))
|
|
120
|
-
expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, signedIn))).toBe('identity-confirmation-required')
|
|
121
|
-
|
|
122
|
-
const leftAlone = browser()
|
|
123
|
-
confirmation.rememberConfirmation({ cookies: leftAlone.cookies }, KIM.id)
|
|
124
|
-
confirmation.forgetConfirmation({ cookies: leftAlone.cookies })
|
|
125
|
-
expect(refusalOf(await run(buildSchema(), 'myPayslips', KIM, leftAlone))).toBe('identity-confirmation-required')
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('asks the privilege first - a person without it is refused for the privilege, confirmed or not', async () => {
|
|
129
|
-
User.hasPrivilege = jest.fn().mockResolvedValue(false)
|
|
130
|
-
const at = browser()
|
|
131
|
-
confirmation.rememberConfirmation({ cookies: at.cookies }, KIM.id)
|
|
132
|
-
|
|
133
|
-
const result = await run(buildSchema(), 'myPayslips', KIM, at)
|
|
134
|
-
expect(result.errors?.[0]?.extensions?.code).not.toBe('IDENTITY_CONFIRMATION_REQUIRED')
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('leaves a door that does not ask alone', async () => {
|
|
138
|
-
const result = await run(buildSchema(), 'myLeave', KIM, browser())
|
|
139
|
-
|
|
140
|
-
expect(result.errors).toBeUndefined()
|
|
141
|
-
expect(result.data).toEqual({ myLeave: 'leave' })
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('refuses to build a door that asks for the confirmation without a privilege beside it', () => {
|
|
145
|
-
expect(() => buildSchema('type Query { myPayslips: String @recentConfirmation myLeave: String }')).toThrow('has no @privilege beside it')
|
|
146
|
-
})
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A list reads this domain's rows only - whatever `inherited` the caller sent (2026-10-02).
|
|
3
|
-
*
|
|
4
|
-
* `getQueryBuilderFromListParams` honours `inherited: Include | Only`, so a list that hands the client's ListParam
|
|
5
|
-
* through lets a child domain read its parent's rows. For these lists a parent's rows are another tenant's
|
|
6
|
-
* configuration or people, and no screen asks for them: the query pins `inherited` to None. The same pin as
|
|
7
|
-
* `accessRequests` (access-request-db.test.ts).
|
|
8
|
-
*
|
|
9
|
-
* Roles are not here - the role screens ask for the parent's roles on purpose (assignable scope, docs/design/auth-rebuild.md).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
13
|
-
import { loadCompiled } from './compiled'
|
|
14
|
-
|
|
15
|
-
const { User } = loadCompiled('service/user/user')
|
|
16
|
-
const { AuthProvider } = loadCompiled('service/auth-provider/auth-provider')
|
|
17
|
-
const { AuthProviderQuery } = loadCompiled('service/auth-provider/auth-provider-query')
|
|
18
|
-
const { Appliance } = loadCompiled('service/appliance/appliance')
|
|
19
|
-
const { ApplianceQuery } = loadCompiled('service/appliance/appliance-query')
|
|
20
|
-
const { Application } = loadCompiled('service/application/application')
|
|
21
|
-
const { ApplicationQuery } = loadCompiled('service/application/application-query')
|
|
22
|
-
const { Partner } = loadCompiled('service/partner/partner')
|
|
23
|
-
const { PartnerQuery } = loadCompiled('service/partner/partner-query')
|
|
24
|
-
|
|
25
|
-
let parent: any
|
|
26
|
-
let child: any
|
|
27
|
-
let partnerDomain: any
|
|
28
|
-
let parentUser: any
|
|
29
|
-
let childUser: any
|
|
30
|
-
|
|
31
|
-
async function makeUser(name: string, domains: any[]) {
|
|
32
|
-
return await getRepository(User).save({
|
|
33
|
-
username: name,
|
|
34
|
-
email: `${name}@example.com`,
|
|
35
|
-
name,
|
|
36
|
-
salt: User.generateSalt(),
|
|
37
|
-
status: 'activated',
|
|
38
|
-
domains
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const from = (domain: any, user: any) => ({ state: { domain, user } }) as any
|
|
43
|
-
const ONLY = { inherited: 'Only' } as any
|
|
44
|
-
|
|
45
|
-
beforeAll(async () => {
|
|
46
|
-
await openAuthDatabase()
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
afterAll(async () => {
|
|
50
|
-
await closeAuthDatabase()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
beforeEach(async () => {
|
|
54
|
-
await resetAuthDatabase()
|
|
55
|
-
|
|
56
|
-
const savedParent = await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
|
|
57
|
-
parent = await getRepository(Domain).findOneBy({ id: savedParent.id })
|
|
58
|
-
const savedChild = await getRepository(Domain).save({ name: 'Plant A-1', subdomain: 'plant-a-1', parent })
|
|
59
|
-
child = await getRepository(Domain).findOneBy({ id: savedChild.id })
|
|
60
|
-
partnerDomain = await getRepository(Domain).save({ name: 'Vendor', subdomain: 'vendor' })
|
|
61
|
-
|
|
62
|
-
parentUser = await makeUser('parent-admin', [parent])
|
|
63
|
-
childUser = await makeUser('child-admin', [child])
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
/*
|
|
67
|
-
* Each list: the parent reads its own row (so an empty answer below is the pin, not a broken fixture), and the
|
|
68
|
-
* child asking for the parent's with `inherited: Only` reads nothing.
|
|
69
|
-
*/
|
|
70
|
-
const LISTS: [string, () => Promise<any>, (params: any, context: any) => Promise<any>][] = [
|
|
71
|
-
[
|
|
72
|
-
'auth providers',
|
|
73
|
-
() => getRepository(AuthProvider).save({ domain: parent, type: 'keycloak', clientSecret: 'parent-secret' }),
|
|
74
|
-
(params, context) => new AuthProviderQuery().authProviders(params, context)
|
|
75
|
-
],
|
|
76
|
-
[
|
|
77
|
-
'appliances',
|
|
78
|
-
() => getRepository(Appliance).save({ domain: parent, name: 'gate', serialNo: 'gate-SN', brand: 'Acme', model: 'X1' }),
|
|
79
|
-
(params, context) => new ApplianceQuery().appliances(params, context)
|
|
80
|
-
],
|
|
81
|
-
[
|
|
82
|
-
'edges',
|
|
83
|
-
() => getRepository(Appliance).save({ domain: parent, name: 'edge', serialNo: 'edge-SN', brand: 'Acme', model: 'X1' }),
|
|
84
|
-
(params, context) => new ApplianceQuery().edges(params, context)
|
|
85
|
-
],
|
|
86
|
-
[
|
|
87
|
-
'applications',
|
|
88
|
-
() =>
|
|
89
|
-
getRepository(Application).save({
|
|
90
|
-
domain: parent,
|
|
91
|
-
name: 'billing',
|
|
92
|
-
email: 'billing@acme.z',
|
|
93
|
-
url: 'https://billing.example.com',
|
|
94
|
-
redirectUrl: 'https://billing.example.com/callback',
|
|
95
|
-
status: 0,
|
|
96
|
-
type: 'OTHERS',
|
|
97
|
-
appSecret: 'parent-app-secret'
|
|
98
|
-
}),
|
|
99
|
-
(params, context) => new ApplicationQuery().applications(params, context)
|
|
100
|
-
],
|
|
101
|
-
[
|
|
102
|
-
'partners',
|
|
103
|
-
() => getRepository(Partner).save({ domain: parent, partnerDomain, requestedAt: new Date() }),
|
|
104
|
-
(params, context) => new PartnerQuery().partners(params, context)
|
|
105
|
-
]
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
describe.each(LISTS)('%s', (_name, seed, list) => {
|
|
109
|
-
it("★ the parent reads its own row - the fixture is real", async () => {
|
|
110
|
-
await seed()
|
|
111
|
-
const answer = await list({}, from(parent, parentUser))
|
|
112
|
-
expect(answer.items).toHaveLength(1)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it("★ a child domain asking for the parent's with inherited: Only reads nothing", async () => {
|
|
116
|
-
await seed()
|
|
117
|
-
const answer = await list(ONLY, from(child, childUser))
|
|
118
|
-
expect(answer.items).toEqual([])
|
|
119
|
-
expect(answer.total).toBe(0)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it("a child domain asking with inherited: Include reads nothing of the parent's", async () => {
|
|
123
|
-
await seed()
|
|
124
|
-
const answer = await list({ inherited: 'Include' }, from(child, childUser))
|
|
125
|
-
expect(answer.items).toEqual([])
|
|
126
|
-
})
|
|
127
|
-
})
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 잠긴 계정이 **어떤 경로로 풀리나** — 두 메일이 서로 다른 곳으로 간다.
|
|
3
|
-
*
|
|
4
|
-
* 계정이 잠기면 두 종류의 메일이 있을 수 있고, 템플릿이 거의 같아 눈으로는 잘 안 갈린다
|
|
5
|
-
* (둘 다 `<title>reset password</title>` · 같은 레이아웃 · 본문 한 줄만 다르다). 구별되는 것은
|
|
6
|
-
* 메일 **제목**뿐이다 — `Reset your password` 와 `Your account is locked`.
|
|
7
|
-
*
|
|
8
|
-
* 그런데 **결과가 다르다.** 비슷하게 보이면 안 되는 짝이라는 것을 여기서 못 박는다.
|
|
9
|
-
* 템플릿 자체의 개선은 따로 대기 중이다.
|
|
10
|
-
*
|
|
11
|
-
* (관리자 경로 `activateUser` 는 `failCount` 를 0 으로 두고 `ACTIVATED` 로 되돌린다 —
|
|
12
|
-
* type-graphql 리졸버라 jest 에서 들 수 없어 여기서는 시험하지 않는다. 그 자리가 정식 경로다.)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
16
|
-
import { loadCompiled } from './compiled'
|
|
17
|
-
|
|
18
|
-
const { User } = loadCompiled('service/user/user')
|
|
19
|
-
const { signin } = loadCompiled('controllers/signin')
|
|
20
|
-
const { resetPassword } = loadCompiled('controllers/reset-password')
|
|
21
|
-
const { unlockUser } = loadCompiled('controllers/unlock-user')
|
|
22
|
-
const { VerificationToken, VerificationTokenType } = loadCompiled('service/verification-token/verification-token')
|
|
23
|
-
const { saveVerificationToken } = loadCompiled('controllers/utils/save-verification-token')
|
|
24
|
-
|
|
25
|
-
const OLD = 'Passw0rd!'
|
|
26
|
-
const NEW = 'N3wPassw0rd!'
|
|
27
|
-
|
|
28
|
-
let domain: any
|
|
29
|
-
|
|
30
|
-
async function lockedUser() {
|
|
31
|
-
const salt = User.generateSalt()
|
|
32
|
-
|
|
33
|
-
return await getRepository(User).save({
|
|
34
|
-
username: 'kim',
|
|
35
|
-
email: 'kim@example.com',
|
|
36
|
-
name: 'Kim',
|
|
37
|
-
salt,
|
|
38
|
-
password: User.encode(OLD, salt),
|
|
39
|
-
status: 'locked',
|
|
40
|
-
failCount: 5,
|
|
41
|
-
domains: [domain]
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* Through the one writer, so the row carries its hash and expiry the way a mailed link's does. */
|
|
46
|
-
async function giveToken(userId: string, token: string, type: string) {
|
|
47
|
-
await saveVerificationToken(userId, token, type)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** `resetPassword` 가 컨텍스트에서 읽는 것만 담는다. */
|
|
51
|
-
const context = { t: (key: string) => key, lng: 'en-US' }
|
|
52
|
-
|
|
53
|
-
beforeAll(async () => {
|
|
54
|
-
await openAuthDatabase()
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
beforeEach(async () => {
|
|
58
|
-
await resetAuthDatabase()
|
|
59
|
-
domain = await getRepository(Domain).save({ name: 'plant-a', subdomain: 'plant-a' })
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
afterAll(async () => {
|
|
63
|
-
await closeAuthDatabase()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
describe('재설정 메일 경로 (PASSWORD_RESET)', () => {
|
|
67
|
-
it('비밀번호는 바뀌지만 **잠금은 그대로다**', async () => {
|
|
68
|
-
/* `resetPassword` 는 `status` 를 건드리지 않는다 — 그 파일의 INACTIVE 검사도 주석 처리되어
|
|
69
|
-
있다. 그래서 이 메일은 「비밀번호를 잊었다」를 위한 것이고 「잠겼다」의 답이 아니다. */
|
|
70
|
-
const user = await lockedUser()
|
|
71
|
-
await giveToken(user.id, 'tok', VerificationTokenType.PASSWORD_RESET)
|
|
72
|
-
|
|
73
|
-
await resetPassword('tok', NEW, 'kim@example.com', context)
|
|
74
|
-
|
|
75
|
-
const after = await getRepository(User).findOneBy({ id: user.id })
|
|
76
|
-
expect(User.verify(after.password, NEW, after.salt)).toBe(true)
|
|
77
|
-
expect(after.status).toBe('locked')
|
|
78
|
-
expect(after.failCount).toBe(5)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('그래서 새 비밀번호로도 들어갈 수 없다', async () => {
|
|
82
|
-
/* `signin` 의 잠금 검사가 비밀번호 대조보다 먼저다. 사용자가 겪는 것은
|
|
83
|
-
「메일로 비밀번호를 바꿨는데 여전히 못 들어간다」다. */
|
|
84
|
-
const user = await lockedUser()
|
|
85
|
-
await giveToken(user.id, 'tok', VerificationTokenType.PASSWORD_RESET)
|
|
86
|
-
|
|
87
|
-
await resetPassword('tok', NEW, 'kim@example.com', context)
|
|
88
|
-
|
|
89
|
-
await expect(signin({ username: 'kim', password: NEW })).rejects.toThrow()
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
describe('잠금 해제 메일 경로 (UNLOCK)', () => {
|
|
94
|
-
it('비밀번호를 바꾸고 잠금까지 되돌린다', async () => {
|
|
95
|
-
const user = await lockedUser()
|
|
96
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
97
|
-
|
|
98
|
-
expect(await unlockUser('tok', NEW)).toBe(true)
|
|
99
|
-
|
|
100
|
-
const after = await getRepository(User).findOneBy({ id: user.id })
|
|
101
|
-
expect(after.status).toBe('activated')
|
|
102
|
-
expect(after.failCount).toBe(0)
|
|
103
|
-
expect(User.verify(after.password, NEW, after.salt)).toBe(true)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('풀린 뒤에는 새 비밀번호로 들어간다', async () => {
|
|
107
|
-
const user = await lockedUser()
|
|
108
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
109
|
-
|
|
110
|
-
await unlockUser('tok', NEW)
|
|
111
|
-
|
|
112
|
-
const result = await signin({ username: 'kim', password: NEW })
|
|
113
|
-
expect(result.token).toBeTruthy()
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('토큰을 한 번만 쓸 수 있다 — 두 번째는 거절한다(던지지 않는다)', async () => {
|
|
117
|
-
const user = await lockedUser()
|
|
118
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
119
|
-
|
|
120
|
-
await unlockUser('tok', NEW)
|
|
121
|
-
|
|
122
|
-
/* 예전에는 토큰 행이 사라진 뒤 `findOne` 의 `null` 을 구조 분해해서 던졌고, 화면은 이유가
|
|
123
|
-
아니라 500 을 봤다. 지금은 거절로 돌아온다. */
|
|
124
|
-
expect(await unlockUser('tok', NEW)).toBe(false)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('잠기지 않은 계정에는 아무 일도 하지 않는다', async () => {
|
|
128
|
-
const user = await lockedUser()
|
|
129
|
-
await getRepository(User).update({ id: user.id }, { status: 'activated', failCount: 0 })
|
|
130
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
131
|
-
|
|
132
|
-
/* `status != LOCKED` 이면 `false` 를 돌려준다(unlock-user.ts:48) — 비밀번호도 안 바뀐다. */
|
|
133
|
-
expect(await unlockUser('tok', NEW)).toBe(false)
|
|
134
|
-
|
|
135
|
-
const after = await getRepository(User).findOneBy({ id: user.id })
|
|
136
|
-
expect(User.verify(after.password, OLD, after.salt)).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
describe('토큰 없는 요청 — 계정 탈취 회귀', () => {
|
|
141
|
-
/*
|
|
142
|
-
* ── 무엇이 가능했나 (2026-09-09, 고침) ──────────────────────────────────────
|
|
143
|
-
* `unlockUser` 가 `findOne({ where: { token, type: UNLOCK } })` 로 찾았는데, TypeORM 은
|
|
144
|
-
* `where` 에서 **`undefined` 조건을 떨어뜨린다.** 그래서 `token` 이 없으면 조건이
|
|
145
|
-
* `{ type: UNLOCK }` 만 남아 **누구의 해제 토큰이든** 맞았고, 그 계정의 비밀번호를 부르는
|
|
146
|
-
* 쪽이 준 값으로 바꿨다.
|
|
147
|
-
*
|
|
148
|
-
* 두 조각 다 자격 없이 손에 넣을 수 있었다. 남의 계정을 다섯 번 틀리면 잠기면서 해제 토큰이
|
|
149
|
-
* 만들어지고(그것을 막을 rate limit 이 없다), `/auth/unlock-user` 의 가드가
|
|
150
|
-
* `!(token || password)` 여서 **비밀번호만 담은 요청이 통과했다.**
|
|
151
|
-
*
|
|
152
|
-
* 실제로 전 과정을 태워 확인했다 — status `activated` · failCount 0 · 비밀번호는 공격자의
|
|
153
|
-
* 것 · 그 비밀번호로 로그인 성공.
|
|
154
|
-
*
|
|
155
|
-
* 두 자리를 다 고쳤다(컨트롤러의 요구 조건 · 라우터의 가드). 아래는 컨트롤러 쪽을 지킨다 —
|
|
156
|
-
* 라우터 가드가 되돌아가도 여기서 막힌다.
|
|
157
|
-
*/
|
|
158
|
-
it('토큰 없이 부르면 잠긴 계정을 건드리지 못한다', async () => {
|
|
159
|
-
const user = await lockedUser()
|
|
160
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
161
|
-
|
|
162
|
-
expect(await unlockUser(undefined, 'ATTACKERpass1!')).toBe(false)
|
|
163
|
-
|
|
164
|
-
const after = await getRepository(User).findOneBy({ id: user.id })
|
|
165
|
-
expect(after.status).toBe('locked')
|
|
166
|
-
expect(after.failCount).toBe(5)
|
|
167
|
-
expect(User.verify(after.password, 'ATTACKERpass1!', after.salt)).toBe(false)
|
|
168
|
-
expect(User.verify(after.password, OLD, after.salt)).toBe(true)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('빈 문자열 토큰도 같다', async () => {
|
|
172
|
-
const user = await lockedUser()
|
|
173
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
174
|
-
|
|
175
|
-
expect(await unlockUser('', 'ATTACKERpass1!')).toBe(false)
|
|
176
|
-
|
|
177
|
-
const after = await getRepository(User).findOneBy({ id: user.id })
|
|
178
|
-
expect(after.status).toBe('locked')
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('그 뒤에도 공격자의 비밀번호로 들어갈 수 없다', async () => {
|
|
182
|
-
const user = await lockedUser()
|
|
183
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
184
|
-
|
|
185
|
-
await unlockUser(undefined, 'ATTACKERpass1!')
|
|
186
|
-
|
|
187
|
-
await expect(signin({ username: 'kim', password: 'ATTACKERpass1!' })).rejects.toThrow()
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('비밀번호 없이 부르면 거절한다 — 던지지 않는다', async () => {
|
|
191
|
-
/* 예전에는 `User.encode(undefined, salt)` 가 `crypto` 안에서 던져 500 이 됐다. */
|
|
192
|
-
const user = await lockedUser()
|
|
193
|
-
await giveToken(user.id, 'tok', VerificationTokenType.UNLOCK)
|
|
194
|
-
|
|
195
|
-
expect(await unlockUser('tok', undefined)).toBe(false)
|
|
196
|
-
|
|
197
|
-
const after = await getRepository(User).findOneBy({ id: user.id })
|
|
198
|
-
expect(after.status).toBe('locked')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('없는 토큰은 거절한다 — 던지지 않는다', async () => {
|
|
202
|
-
await lockedUser()
|
|
203
|
-
|
|
204
|
-
expect(await unlockUser('no-such-token', NEW)).toBe(false)
|
|
205
|
-
})
|
|
206
|
-
})
|