@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,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The access request's AI 입력 declarations (ADR-0103 decision 4).
|
|
3
|
-
*
|
|
4
|
-
* The model drafts and a person presses; the card's check is the review the list screen asks. That last point is
|
|
5
|
-
* what this file holds above all: the card's findings for a decision equal `reviewDecision` for the same decision.
|
|
6
|
-
*
|
|
7
|
-
* The declarations are read from the compiled package so their check reaches the same entities as the database.
|
|
8
|
-
* The assistant's own validation is asked through its source file, which needs only `graphql`.
|
|
9
|
-
*/
|
|
10
|
-
import { parse } from 'graphql'
|
|
11
|
-
|
|
12
|
-
import { clearEntryTargets, declareEntryTarget } from '../../ai-assistant/server/service/data-entry/entry-target'
|
|
13
|
-
import { applyEntryProposal } from '../../ai-assistant/server/service/data-entry/entry-apply'
|
|
14
|
-
|
|
15
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
16
|
-
import { loadCompiled } from './compiled'
|
|
17
|
-
|
|
18
|
-
import uiEn from '../../auth-ui/translations/en.json'
|
|
19
|
-
import uiKo from '../../auth-ui/translations/ko.json'
|
|
20
|
-
|
|
21
|
-
const { Role } = loadCompiled('service/role/role')
|
|
22
|
-
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
23
|
-
const { User } = loadCompiled('service/user/user')
|
|
24
|
-
const access = loadCompiled('controllers/access-request')
|
|
25
|
-
const targets = loadCompiled('ai-entry/access-entry-targets')
|
|
26
|
-
const { judgePermission } = loadCompiled('utils/check-permission')
|
|
27
|
-
const { AccessRequest } = loadCompiled('service/access-request/access-request')
|
|
28
|
-
|
|
29
|
-
const rootFields = (document: string) =>
|
|
30
|
-
((parse(document).definitions[0] as any).selectionSet.selections as any[]).map(one => one.name.value)
|
|
31
|
-
|
|
32
|
-
const row = (values: Record<string, unknown>, index = 0) => ({ index, values, sources: [], issues: [] })
|
|
33
|
-
|
|
34
|
-
describe('the declarations', () => {
|
|
35
|
-
afterEach(() => clearEntryTargets())
|
|
36
|
-
|
|
37
|
-
it('the built public AUTH entry exports the same refusal formatter used by AI entry', () => {
|
|
38
|
-
expect(loadCompiled('index').privilegeRejection).toBe(loadCompiled('service/privilege/privilege-rejection').privilegeRejection)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('are accepted by the assistant as declared', () => {
|
|
42
|
-
for (const target of targets.accessEntryTargets()) expect(() => declareEntryTarget(target)).not.toThrow()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('★ the decision card writes through decideAccessRequest - the person presses it; nothing else is a door', () => {
|
|
46
|
-
const decision = targets.accessDecisionEntryTarget()
|
|
47
|
-
expect(rootFields(decision.mutation)).toEqual(['decideAccessRequest'])
|
|
48
|
-
expect(rootFields(decision.fields.find((f: any) => f.name === 'request').reference.query)).toEqual(['accessRequests'])
|
|
49
|
-
expect(rootFields(targets.accessRequestEntryTarget().mutation)).toEqual(['requestAccess'])
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('a denial carries no role, a grant carries its role id', () => {
|
|
53
|
-
expect(targets.decisionOf(row({ request: { id: 'q' }, decision: 'DENY', role: { id: 'r' }, reason: 'no' }))).toEqual({
|
|
54
|
-
requestId: 'q',
|
|
55
|
-
decision: 'DENY',
|
|
56
|
-
roleId: undefined,
|
|
57
|
-
reason: 'no'
|
|
58
|
-
})
|
|
59
|
-
expect(targets.decisionOf(row({ request: { id: 'q' }, decision: 'GRANT', role: { id: 'r' } })).roleId).toBe('r')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('a request is named by person or privilege; only pending ones; several stay several', () => {
|
|
63
|
-
const items = [
|
|
64
|
-
{ id: '1', state: 'PENDING', category: 'warehouse', privilege: 'mutation', requester: { name: '김민수' } },
|
|
65
|
-
{ id: '2', state: 'PENDING', category: 'board', privilege: 'query', requester: { name: '김민지' } },
|
|
66
|
-
{ id: '3', state: 'GRANTED', category: 'warehouse', privilege: 'mutation', requester: { name: '김민수' } }
|
|
67
|
-
]
|
|
68
|
-
expect(targets.requestsNamed(items, '김민수').map((r: any) => r.id)).toEqual(['1'])
|
|
69
|
-
expect(targets.requestsNamed(items, '김민').map((r: any) => r.id)).toEqual(['1', '2'])
|
|
70
|
-
expect(targets.requestsNamed(items, 'board:query').map((r: any) => r.label)).toEqual(['김민지 · board:query'])
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('every word the card shows is in the screen words', () => {
|
|
74
|
-
const decision = targets.accessDecisionEntryTarget()
|
|
75
|
-
const keys = [
|
|
76
|
-
decision.labelKey,
|
|
77
|
-
targets.accessRequestEntryTarget().labelKey,
|
|
78
|
-
...[...decision.fields, ...targets.accessRequestEntryTarget().fields].map((f: any) => f.labelKey),
|
|
79
|
-
'access-request.decision.grant',
|
|
80
|
-
'access-request.decision.deny',
|
|
81
|
-
'access-request.issue.not-by-role'
|
|
82
|
-
]
|
|
83
|
-
for (const dict of [uiEn, uiKo] as Record<string, string>[]) for (const key of keys) expect([key, typeof dict[key]]).toEqual([key, 'string'])
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('the card says what the screen says', () => {
|
|
88
|
-
let domain: any
|
|
89
|
-
let kim: any
|
|
90
|
-
let park: any
|
|
91
|
-
let inbound: any
|
|
92
|
-
let manager: any
|
|
93
|
-
|
|
94
|
-
beforeAll(async () => {
|
|
95
|
-
await openAuthDatabase()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
afterAll(async () => {
|
|
99
|
-
await closeAuthDatabase()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
beforeEach(async () => {
|
|
103
|
-
await resetAuthDatabase()
|
|
104
|
-
const saved = await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
|
|
105
|
-
domain = await getRepository(Domain).findOneBy({ id: saved.id })
|
|
106
|
-
const mutation = await getRepository(Privilege).save({ name: 'mutation', category: 'warehouse' })
|
|
107
|
-
const query = await getRepository(Privilege).save({ name: 'query', category: 'warehouse' })
|
|
108
|
-
const grant = await getRepository(Privilege).save({ name: 'mutation', category: 'user' })
|
|
109
|
-
inbound = await getRepository(Role).save({ name: 'inbound', domain, privileges: [mutation] })
|
|
110
|
-
manager = await getRepository(Role).save({ name: 'manager', domain, privileges: [mutation, query, grant] })
|
|
111
|
-
const admins = await getRepository(Role).save({ name: 'admins', domain, privileges: [grant] })
|
|
112
|
-
const make = (name: string, roles: any[]) =>
|
|
113
|
-
getRepository(User).save({ username: name, email: `${name}@example.com`, name, salt: User.generateSalt(), status: 'activated', domains: [domain], roles })
|
|
114
|
-
kim = await make('kim', [])
|
|
115
|
-
park = await make('park', [admins])
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
const state = (user: any) => ({ domain, user })
|
|
119
|
-
|
|
120
|
-
it.each(['GRANT', 'DENY'])('a stored AI decision applies %s through the same controller and schedules the result after commit', async decision => {
|
|
121
|
-
const request = await access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, { state: { ...state(kim), afterCommit: [] } } as any)
|
|
122
|
-
const target = targets.accessDecisionEntryTarget()
|
|
123
|
-
/* notification's `sendNotice` (ADR-0164 amendment 1), seen as who, where and what the notice says */
|
|
124
|
-
const notify = jest.fn()
|
|
125
|
-
const sendNotice = (notice: any) => notify({ receivers: notice.receivers.map((one: any) => one.id), url: notice.address, title: notice.words('ko').subject })
|
|
126
|
-
const afterCommit: any[] = []
|
|
127
|
-
const context = { state: { ...state(park), sendNotice, afterCommit }, t: (key: string) => key }
|
|
128
|
-
const execute = jest.fn(async (document, variables) => {
|
|
129
|
-
expect(document).toBe(target.mutation)
|
|
130
|
-
const decided = await access.decideAccessRequest(variables.decision, context)
|
|
131
|
-
return { data: { decideAccessRequest: decided } }
|
|
132
|
-
})
|
|
133
|
-
const proposal = { key: 'ai-decision', kind: 'entry', target: target.key, rows: [row({ request: { id: request.id }, decision, ...(decision === 'GRANT' ? { role: { id: inbound.id } } : { reason: 'IT only' }) })] }
|
|
134
|
-
const outcome = await applyEntryProposal(proposal.key, {
|
|
135
|
-
messages: [{ id: 'm', proposals: [proposal] }], answered: false, state: context.state,
|
|
136
|
-
passesDoor: async door => {
|
|
137
|
-
expect(door).toBe('Mutation.decideAccessRequest')
|
|
138
|
-
return (await judgePermission({ category: 'user', privilege: 'mutation', owner: true }, park, domain)).permitted
|
|
139
|
-
},
|
|
140
|
-
execute
|
|
141
|
-
}, key => key === target.key ? target : undefined)
|
|
142
|
-
expect(outcome).toMatchObject({ verdict: 'accepted', applied: 1, missed: 0 })
|
|
143
|
-
expect(execute).toHaveBeenCalledTimes(1)
|
|
144
|
-
expect(notify).not.toHaveBeenCalled()
|
|
145
|
-
for (const step of afterCommit) await step.run()
|
|
146
|
-
expect(notify).toHaveBeenCalledWith(expect.objectContaining({ receivers: [kim.id], url: 'my-access-requests', title: `access-request.notify.${decision === 'GRANT' ? 'granted' : 'denied'}.title` }))
|
|
147
|
-
expect((await getRepository(AccessRequest).findOneBy({ id: request.id })).state).toBe(decision === 'GRANT' ? 'GRANTED' : 'DENIED')
|
|
148
|
-
expect(await User.hasPrivilege('mutation', 'warehouse', domain, kim)).toBe(decision === 'GRANT')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('★ the card\'s findings are reviewDecision\'s, for grant and for deny', async () => {
|
|
152
|
-
const request = await access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, { state: { ...state(kim), afterCommit: [] } } as any)
|
|
153
|
-
const rows = [
|
|
154
|
-
row({ request: { id: request.id }, decision: 'GRANT', role: { id: manager.id } }, 0),
|
|
155
|
-
row({ request: { id: request.id }, decision: 'GRANT', role: { id: inbound.id } }, 1),
|
|
156
|
-
row({ request: { id: request.id }, decision: 'DENY', reason: '' }, 2)
|
|
157
|
-
]
|
|
158
|
-
|
|
159
|
-
const card = await targets.accessDecisionEntryTarget().check(rows, { state: state(park) })
|
|
160
|
-
|
|
161
|
-
for (const [index, one] of rows.entries()) {
|
|
162
|
-
const screen = await access.reviewDecision(targets.decisionOf(one), { state: state(park) } as any)
|
|
163
|
-
expect(card[index].map((issue: any) => [issue.code, issue.blocking])).toEqual(screen.map((issue: any) => [issue.code, issue.blocking]))
|
|
164
|
-
}
|
|
165
|
-
expect(card[0].map((issue: any) => issue.code)).toContain('role-grants-admin')
|
|
166
|
-
expect(card[2].map((issue: any) => issue.code)).toEqual(['deny-needs-reason'])
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('★ the request card refuses what the door would - already held', async () => {
|
|
170
|
-
const lee = await getRepository(User).save({
|
|
171
|
-
username: 'lee', email: 'lee@example.com', name: 'lee', salt: User.generateSalt(), status: 'activated', domains: [domain], roles: [inbound]
|
|
172
|
-
})
|
|
173
|
-
const [issues] = await targets.accessRequestEntryTarget().check([row({ category: 'warehouse', privilege: 'mutation' })], { state: state(lee) })
|
|
174
|
-
expect(issues).toEqual([expect.objectContaining({ code: 'already-holds', blocking: true })])
|
|
175
|
-
})
|
|
176
|
-
})
|
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Asking, deciding, telling - the access request end to end on the database (ADR-0103).
|
|
3
|
-
*
|
|
4
|
-
* The controller is called with a request state as the doors receive it. `sendNotice` is a spy: the notification
|
|
5
|
-
* module is optional and the row is the record.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
9
|
-
import { loadCompiled } from './compiled'
|
|
10
|
-
|
|
11
|
-
const { Role } = loadCompiled('service/role/role')
|
|
12
|
-
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
13
|
-
const { User } = loadCompiled('service/user/user')
|
|
14
|
-
const { DomainOwner } = loadCompiled('service/domain-owner/domain-owner')
|
|
15
|
-
const { AccessRequest } = loadCompiled('service/access-request/access-request')
|
|
16
|
-
const access = loadCompiled('controllers/access-request')
|
|
17
|
-
|
|
18
|
-
let domain: any
|
|
19
|
-
let other: any
|
|
20
|
-
let warehouseMutation: any
|
|
21
|
-
let warehouseQuery: any
|
|
22
|
-
let userMutation: any
|
|
23
|
-
let inbound: any
|
|
24
|
-
let manager: any
|
|
25
|
-
let admins: any
|
|
26
|
-
let kim: any
|
|
27
|
-
let park: any
|
|
28
|
-
let owner: any
|
|
29
|
-
|
|
30
|
-
async function makeUser(name: string, domains: any[], roles: any[] = [], over: any = {}) {
|
|
31
|
-
const tag = Math.random().toString(36).slice(2, 8)
|
|
32
|
-
return await getRepository(User).save({
|
|
33
|
-
username: `${name}-${tag}`,
|
|
34
|
-
email: `${name}-${tag}@example.com`,
|
|
35
|
-
name,
|
|
36
|
-
salt: User.generateSalt(),
|
|
37
|
-
status: 'activated',
|
|
38
|
-
domains,
|
|
39
|
-
roles,
|
|
40
|
-
...over
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/* notification's `sendNotice` (ADR-0164 amendment 1), seen as who, where and what the notice says */
|
|
45
|
-
const noticeSpy = () => {
|
|
46
|
-
const told = jest.fn()
|
|
47
|
-
return { told, sendNotice: (notice: any) => told({ receivers: notice.receivers.map((one: any) => one.id), url: notice.address, title: notice.words('ko').subject }) }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function contextOf(user: any, over: any = {}) {
|
|
51
|
-
const { told: notify, sendNotice } = noticeSpy()
|
|
52
|
-
const afterCommit: any[] = []
|
|
53
|
-
return {
|
|
54
|
-
state: { domain, user, afterCommit, sendNotice, ...over },
|
|
55
|
-
t: (key: string) => key,
|
|
56
|
-
afterCommit,
|
|
57
|
-
notify
|
|
58
|
-
} as any
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function commit(context: any) {
|
|
62
|
-
for (const step of context.afterCommit) await step.run()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function holds(user: any, category: string, privilege: string) {
|
|
66
|
-
return await User.hasPrivilege(privilege, category, domain, user)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
beforeAll(async () => {
|
|
70
|
-
await openAuthDatabase()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
afterAll(async () => {
|
|
74
|
-
await closeAuthDatabase()
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
beforeEach(async () => {
|
|
78
|
-
await resetAuthDatabase()
|
|
79
|
-
|
|
80
|
-
const saved = await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
|
|
81
|
-
domain = await getRepository(Domain).findOneBy({ id: saved.id })
|
|
82
|
-
other = await getRepository(Domain).save({ name: 'Other', subdomain: 'other' })
|
|
83
|
-
|
|
84
|
-
warehouseMutation = await getRepository(Privilege).save({ name: 'mutation', category: 'warehouse' })
|
|
85
|
-
warehouseQuery = await getRepository(Privilege).save({ name: 'query', category: 'warehouse' })
|
|
86
|
-
userMutation = await getRepository(Privilege).save({ name: 'mutation', category: 'user' })
|
|
87
|
-
|
|
88
|
-
inbound = await getRepository(Role).save({ name: 'inbound', domain, privileges: [warehouseMutation] })
|
|
89
|
-
manager = await getRepository(Role).save({ name: 'manager', domain, privileges: [warehouseMutation, warehouseQuery, userMutation] })
|
|
90
|
-
admins = await getRepository(Role).save({ name: 'admins', domain, privileges: [userMutation, warehouseQuery] })
|
|
91
|
-
|
|
92
|
-
kim = await makeUser('kim', [domain])
|
|
93
|
-
park = await makeUser('park', [domain], [admins])
|
|
94
|
-
owner = await makeUser('owner', [domain])
|
|
95
|
-
await getRepository(DomainOwner).save({ domain, user: owner })
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const ASK = { category: 'warehouse', privilege: 'mutation', operationName: 'updateInboundPlan', reason: 'inbound from today' }
|
|
99
|
-
|
|
100
|
-
describe('asking', () => {
|
|
101
|
-
it('★ a request is a row - pending, fourteen days, and the people who decide are told once', async () => {
|
|
102
|
-
const context = contextOf(kim)
|
|
103
|
-
const request = await access.requestAccess(ASK, context)
|
|
104
|
-
await commit(context)
|
|
105
|
-
|
|
106
|
-
expect(request).toMatchObject({ state: 'PENDING', category: 'warehouse', privilege: 'mutation', count: 1, reason: 'inbound from today' })
|
|
107
|
-
expect(context.notify).toHaveBeenCalledTimes(1)
|
|
108
|
-
expect([...context.notify.mock.calls[0][0].receivers].sort()).toEqual([owner.id, park.id].sort())
|
|
109
|
-
expect(context.notify.mock.calls[0][0].url).toBe(`access-requests?id=${request.id}`)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('★ asking again adds to the pending one - no second row, nobody told again', async () => {
|
|
113
|
-
await access.requestAccess(ASK, contextOf(kim))
|
|
114
|
-
const again = contextOf(kim)
|
|
115
|
-
const merged = await access.requestAccess({ ...ASK, reason: 'still needed' }, again)
|
|
116
|
-
await commit(again)
|
|
117
|
-
|
|
118
|
-
expect(merged.count).toBe(2)
|
|
119
|
-
expect(merged.reason).toBe('still needed')
|
|
120
|
-
expect(await getRepository(AccessRequest).count()).toBe(1)
|
|
121
|
-
expect(again.notify).not.toHaveBeenCalled()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('★ a privilege the caller holds is not asked for', async () => {
|
|
125
|
-
const kimWithRole = await makeUser('lee', [domain], [inbound])
|
|
126
|
-
await expect(access.requestAccess(ASK, contextOf(kimWithRole))).rejects.toMatchObject({ code: 'access-request-already-holds' })
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('★ refused by the license, not by a role - not taken, whatever the screen sent', async () => {
|
|
130
|
-
const context = contextOf(kim, { prohibitedPrivileges: [{ category: 'warehouse', privilege: 'mutation', by: 'license' }] })
|
|
131
|
-
await expect(access.requestAccess(ASK, context)).rejects.toMatchObject({ code: 'access-request-not-by-role', params: { because: 'license' } })
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('a privilege nobody declared is not asked for', async () => {
|
|
135
|
-
await expect(access.requestAccess({ category: 'nothing', privilege: 'here', reason: 'x' }, contextOf(kim))).rejects.toMatchObject({
|
|
136
|
-
code: 'access-request-unknown-privilege'
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('the requester withdraws their own, and nobody else can', async () => {
|
|
141
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
142
|
-
await expect(access.withdrawAccessRequest(request.id, contextOf(park))).rejects.toMatchObject({ code: 'access-request-not-withdrawable' })
|
|
143
|
-
expect((await access.withdrawAccessRequest(request.id, contextOf(kim))).state).toBe('WITHDRAWN')
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
describe('who decides', () => {
|
|
148
|
-
it('★ owners and holders of user:mutation in this domain, never the requester', async () => {
|
|
149
|
-
const outsider = await makeUser('outsider', [other], [admins])
|
|
150
|
-
const ids = await access.findDeciderIds(domain, park.id)
|
|
151
|
-
expect(ids).toEqual([owner.id].sort())
|
|
152
|
-
expect(ids).not.toContain(outsider.id)
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
describe('deciding', () => {
|
|
157
|
-
it('★ grant puts the requester into the role - they pass the gate on the next request', async () => {
|
|
158
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
159
|
-
const context = contextOf(park)
|
|
160
|
-
const decided = await access.decideAccessRequest({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, context)
|
|
161
|
-
await commit(context)
|
|
162
|
-
|
|
163
|
-
expect(decided).toMatchObject({ state: 'GRANTED', grantedRole: { id: inbound.id } })
|
|
164
|
-
expect(await holds(kim, 'warehouse', 'mutation')).toBe(true)
|
|
165
|
-
expect(context.notify).toHaveBeenCalledWith(
|
|
166
|
-
expect.objectContaining({ receivers: [kim.id], title: 'access-request.notify.granted.title', url: 'my-access-requests' })
|
|
167
|
-
)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('★ deny keeps the reason and tells the requester', async () => {
|
|
171
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
172
|
-
const context = contextOf(park)
|
|
173
|
-
const decided = await access.decideAccessRequest({ requestId: request.id, decision: 'DENY', reason: 'IT only' }, context)
|
|
174
|
-
await commit(context)
|
|
175
|
-
|
|
176
|
-
expect(decided).toMatchObject({ state: 'DENIED', decisionReason: 'IT only' })
|
|
177
|
-
expect(await holds(kim, 'warehouse', 'mutation')).toBe(false)
|
|
178
|
-
expect(context.notify).toHaveBeenCalledWith(expect.objectContaining({ title: 'access-request.notify.denied.title' }))
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('★ the second decision is refused - the first one stands', async () => {
|
|
182
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
183
|
-
await access.decideAccessRequest({ requestId: request.id, decision: 'DENY', reason: 'no' }, contextOf(owner))
|
|
184
|
-
await expect(access.decideAccessRequest({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))).rejects.toMatchObject({
|
|
185
|
-
code: 'access-decision-request-not-pending'
|
|
186
|
-
})
|
|
187
|
-
expect(await holds(kim, 'warehouse', 'mutation')).toBe(false)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('★ two people deciding at once - exactly one wins (both reviews read PENDING before either writes)', async () => {
|
|
191
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
192
|
-
const results = await Promise.allSettled([
|
|
193
|
-
access.decideAccessRequest({ requestId: request.id, decision: 'DENY', reason: 'no' }, contextOf(owner)),
|
|
194
|
-
access.decideAccessRequest({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))
|
|
195
|
-
])
|
|
196
|
-
|
|
197
|
-
expect(results.map(r => r.status).sort()).toEqual(['fulfilled', 'rejected'])
|
|
198
|
-
const lost = results.find(r => r.status === 'rejected') as PromiseRejectedResult
|
|
199
|
-
expect(lost.reason).toMatchObject({ code: 'access-decision-request-not-pending' })
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('★ the review blocks inside the door too - a role without the privilege is refused', async () => {
|
|
203
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
204
|
-
await expect(access.decideAccessRequest({ requestId: request.id, decision: 'GRANT', roleId: admins.id }, contextOf(park))).rejects.toMatchObject({
|
|
205
|
-
code: 'access-decision-role-does-not-grant'
|
|
206
|
-
})
|
|
207
|
-
expect((await getRepository(AccessRequest).findOneBy({ id: request.id })).state).toBe('PENDING')
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('★ another tenant\'s role is refused - it is not this domain\'s to give', async () => {
|
|
211
|
-
const foreign = await getRepository(Role).save({ name: 'foreign', domain: other, privileges: [warehouseMutation] })
|
|
212
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
213
|
-
await expect(access.decideAccessRequest({ requestId: request.id, decision: 'GRANT', roleId: foreign.id }, contextOf(park))).rejects.toMatchObject({
|
|
214
|
-
code: 'access-decision-role-not-assignable'
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('a request of another domain is not found from this one', async () => {
|
|
219
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
220
|
-
const stranger = await makeUser('stranger', [other], [admins])
|
|
221
|
-
const fromThere = contextOf(stranger, { domain: await getRepository(Domain).findOneBy({ id: other.id }) })
|
|
222
|
-
await expect(access.decideAccessRequest({ requestId: request.id, decision: 'DENY', reason: 'x' }, fromThere)).rejects.toMatchObject({
|
|
223
|
-
code: 'access-decision-access-request-not-found'
|
|
224
|
-
})
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('★ a grant closes the same person\'s other requests it now answers', async () => {
|
|
228
|
-
const first = await access.requestAccess(ASK, contextOf(kim))
|
|
229
|
-
const second = await access.requestAccess({ category: 'warehouse', privilege: 'query', reason: 'checking stock' }, contextOf(kim))
|
|
230
|
-
await access.decideAccessRequest({ requestId: first.id, decision: 'GRANT', roleId: manager.id }, contextOf(owner))
|
|
231
|
-
|
|
232
|
-
expect((await getRepository(AccessRequest).findOneBy({ id: second.id })).state).toBe('GRANTED')
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
describe('the review as the screen asks it', () => {
|
|
237
|
-
it('★ candidates smallest first, and the review of a big role names what else it gives', async () => {
|
|
238
|
-
const request = await access.requestAccess(ASK, contextOf(kim))
|
|
239
|
-
const candidates = await access.rolesGranting('warehouse', 'mutation', domain)
|
|
240
|
-
expect(candidates.map((c: any) => c.name)).toEqual(['inbound', 'manager'])
|
|
241
|
-
|
|
242
|
-
const issues = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: manager.id }, contextOf(park))
|
|
243
|
-
expect(issues.map((i: any) => i.code).sort()).toEqual(['decider-lacks-it', 'role-grants-admin', 'role-grants-more', 'smaller-role-exists'])
|
|
244
|
-
})
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
describe('asking is not a reflex (questions Q9)', () => {
|
|
248
|
-
it('★ no reason, no request - blank is no reason', async () => {
|
|
249
|
-
for (const reason of [undefined, '', ' ']) {
|
|
250
|
-
await expect(access.requestAccess({ ...ASK, reason }, contextOf(kim))).rejects.toMatchObject({ code: 'access-request-needs-reason' })
|
|
251
|
-
}
|
|
252
|
-
expect(await getRepository(AccessRequest).count()).toBe(0)
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
it('★ denied lately: the same words are refused, new words are taken', async () => {
|
|
256
|
-
const first = await access.requestAccess(ASK, contextOf(kim))
|
|
257
|
-
await access.decideAccessRequest({ requestId: first.id, decision: 'DENY', reason: 'IT only' }, contextOf(park))
|
|
258
|
-
|
|
259
|
-
await expect(access.requestAccess(ASK, contextOf(kim))).rejects.toMatchObject({
|
|
260
|
-
code: 'access-request-same-reason-as-denied',
|
|
261
|
-
params: { reason: 'IT only' }
|
|
262
|
-
})
|
|
263
|
-
const again = await access.requestAccess({ ...ASK, reason: 'moved to the inbound team this week' }, contextOf(kim))
|
|
264
|
-
expect(again.state).toBe('PENDING')
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('a denial older than seven days holds nothing against the same words', async () => {
|
|
268
|
-
const first = await access.requestAccess(ASK, contextOf(kim))
|
|
269
|
-
await access.decideAccessRequest({ requestId: first.id, decision: 'DENY', reason: 'not now' }, contextOf(park))
|
|
270
|
-
await getRepository(AccessRequest).update({ id: first.id }, { decidedAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000) })
|
|
271
|
-
expect((await access.requestAccess(ASK, contextOf(kim))).state).toBe('PENDING')
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('★ the popup can say how many people a request reaches - not who', async () => {
|
|
275
|
-
const { AccessRequestQuery } = loadCompiled('service/access-request/access-request-query')
|
|
276
|
-
expect(await new AccessRequestQuery().accessRequestDeciderCount(contextOf(kim))).toBe(2)
|
|
277
|
-
expect(await new AccessRequestQuery().accessRequestDeciderCount(contextOf(park))).toBe(1)
|
|
278
|
-
})
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
describe('found in review (2026-10-02)', () => {
|
|
282
|
-
const { AccessRequestQuery } = loadCompiled('service/access-request/access-request-query')
|
|
283
|
-
|
|
284
|
-
it('★ the list is this domain\'s only - asking for the parent\'s with inherited: Only reads nothing', async () => {
|
|
285
|
-
await access.requestAccess(ASK, contextOf(kim))
|
|
286
|
-
const savedChild = await getRepository(Domain).save({ name: 'Plant A-1', subdomain: 'plant-a-1', parent: domain })
|
|
287
|
-
const child = await getRepository(Domain).findOneBy({ id: savedChild.id })
|
|
288
|
-
const childAdmin = await makeUser('childadmin', [child])
|
|
289
|
-
const answer = await new AccessRequestQuery().accessRequests({ inherited: 'Only' } as any, { state: { domain: child, user: childAdmin } } as any)
|
|
290
|
-
expect(answer.items).toEqual([])
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('★ ask, withdraw, ask again: one row, and the deciders are told once', async () => {
|
|
294
|
-
const first = contextOf(kim)
|
|
295
|
-
const request = await access.requestAccess(ASK, first)
|
|
296
|
-
await commit(first)
|
|
297
|
-
await access.withdrawAccessRequest(request.id, contextOf(kim))
|
|
298
|
-
const again = contextOf(kim)
|
|
299
|
-
const back = await access.requestAccess({ ...ASK, reason: 'needed after all' }, again)
|
|
300
|
-
await commit(again)
|
|
301
|
-
|
|
302
|
-
expect(back.id).toBe(request.id)
|
|
303
|
-
expect(back.state).toBe('PENDING')
|
|
304
|
-
expect(await getRepository(AccessRequest).count()).toBe(1)
|
|
305
|
-
expect(first.notify).toHaveBeenCalledTimes(1)
|
|
306
|
-
expect(again.notify).not.toHaveBeenCalled()
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('★ a decision may name a parent domain\'s role - the scope the list screen offers', async () => {
|
|
310
|
-
const savedChild = await getRepository(Domain).save({ name: 'Plant A-2', subdomain: 'plant-a-2', parent: domain })
|
|
311
|
-
const child = await getRepository(Domain).findOneBy({ id: savedChild.id })
|
|
312
|
-
const found = await new AccessRequestQuery().assignableRolesNamed('inbound', { state: { domain: child, user: owner } } as any)
|
|
313
|
-
expect(found.map((r: any) => r.id)).toEqual([inbound.id])
|
|
314
|
-
expect(await new AccessRequestQuery().assignableRolesNamed('inbou', { state: { domain: child, user: owner } } as any)).toEqual([])
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('my requests carry their domain and requester - non-null fields are loaded', async () => {
|
|
318
|
-
await access.requestAccess(ASK, contextOf(kim))
|
|
319
|
-
const [mine] = await new AccessRequestQuery().myAccessRequests(contextOf(kim))
|
|
320
|
-
expect(mine.domain?.id).toBe(domain.id)
|
|
321
|
-
expect(mine.requester?.id).toBe(kim.id)
|
|
322
|
-
})
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
describe('asking for a task (ADR-0103 decision 9)', () => {
|
|
326
|
-
const { registerRoleTask } = loadCompiled('service/role-template/role-task')
|
|
327
|
-
const { AccessRequestQuery } = loadCompiled('service/access-request/access-request-query')
|
|
328
|
-
|
|
329
|
-
beforeEach(() => {
|
|
330
|
-
;(process as any)['ROLE_TASKS'] = {}
|
|
331
|
-
;(process as any).PRIVILEGES = {
|
|
332
|
-
'warehouse query': ['warehouse', 'query'],
|
|
333
|
-
'warehouse mutation': ['warehouse', 'mutation'],
|
|
334
|
-
'user mutation': ['user', 'mutation']
|
|
335
|
-
}
|
|
336
|
-
registerRoleTask({ id: 'warehouse.keeping', grants: [{ category: 'warehouse', axes: ['query', 'mutation'] }] })
|
|
337
|
-
registerRoleTask({ id: 'warehouse.reading', grants: [{ category: 'warehouse', axes: ['query'] }] })
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
it('★ the tasks holding a refused privilege are listed fewest privileges first', async () => {
|
|
341
|
-
const query = new AccessRequestQuery()
|
|
342
|
-
expect((await query.tasksGranting('warehouse', 'query')).map((one: any) => [one.taskId, one.privilegeCount])).toEqual([
|
|
343
|
-
['warehouse.reading', 1],
|
|
344
|
-
['warehouse.keeping', 2]
|
|
345
|
-
])
|
|
346
|
-
expect((await query.tasksGranting('warehouse', 'mutation')).map((one: any) => one.taskId)).toEqual(['warehouse.keeping'])
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it('★ the request keeps the task asked for, and refuses one that does not hold the refused privilege', async () => {
|
|
350
|
-
const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
351
|
-
expect(request.taskId).toBe('warehouse.keeping')
|
|
352
|
-
|
|
353
|
-
await expect(access.requestAccess({ ...ASK, taskId: 'warehouse.reading' }, contextOf(park))).rejects.toMatchObject({ code: 'access-request-task-mismatch' })
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
it('★ asking again with other work adds to the pending one and does not change the work it named', async () => {
|
|
357
|
-
await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
358
|
-
registerRoleTask({ id: 'warehouse.receiving', grants: [{ category: 'warehouse', axes: ['mutation'] }] })
|
|
359
|
-
const merged = await access.requestAccess({ ...ASK, taskId: 'warehouse.receiving', reason: 'still needed' }, contextOf(kim))
|
|
360
|
-
|
|
361
|
-
expect(merged.count).toBe(2)
|
|
362
|
-
expect(merged.taskId).toBe('warehouse.keeping')
|
|
363
|
-
expect(await getRepository(AccessRequest).count()).toBe(1)
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
it('★ roles holding the whole task come first, then fewest privileges', async () => {
|
|
367
|
-
expect((await access.rolesGranting('warehouse', 'mutation', domain)).map((c: any) => c.name)).toEqual(['inbound', 'manager'])
|
|
368
|
-
const ordered = await access.rolesGranting('warehouse', 'mutation', domain, undefined, 'warehouse.keeping')
|
|
369
|
-
expect(ordered.map((c: any) => [c.name, c.holdsTask])).toEqual([
|
|
370
|
-
['manager', true],
|
|
371
|
-
['inbound', false]
|
|
372
|
-
])
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
it('★ a role short of the task is told, not refused - and a smaller role short of it is not proposed', async () => {
|
|
376
|
-
const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
377
|
-
|
|
378
|
-
const short = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))
|
|
379
|
-
expect(short.find((i: any) => i.code === 'role-lacks-task')).toMatchObject({ blocking: false, params: { task: 'warehouse.keeping', privileges: 'warehouse:query' } })
|
|
380
|
-
|
|
381
|
-
const whole = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: manager.id }, contextOf(park))
|
|
382
|
-
expect(whole.map((i: any) => i.code)).not.toContain('role-lacks-task')
|
|
383
|
-
expect(whole.map((i: any) => i.code)).not.toContain('smaller-role-exists')
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
it('a task gone from the catalog is no error - the review says nothing of it and no role is put first', async () => {
|
|
387
|
-
const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
388
|
-
;(process as any)['ROLE_TASKS'] = {}
|
|
389
|
-
|
|
390
|
-
const issues = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))
|
|
391
|
-
expect(issues.map((i: any) => i.code)).not.toContain('role-lacks-task')
|
|
392
|
-
expect((await access.rolesGranting('warehouse', 'mutation', domain, undefined, 'warehouse.keeping')).map((c: any) => c.name)).toEqual(['inbound', 'manager'])
|
|
393
|
-
})
|
|
394
|
-
})
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The access request under real concurrency - postgres only (`AUTH_TEST_PG`). sqlite holds one connection and
|
|
3
|
-
* cannot show two transactions racing (feedback: verify concurrency on postgres).
|
|
4
|
-
*
|
|
5
|
-
* Each call runs in its own transaction, as the `@transaction` door gives it.
|
|
6
|
-
*/
|
|
7
|
-
import { authDataSource, closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
8
|
-
import { loadCompiled } from './compiled'
|
|
9
|
-
|
|
10
|
-
const { Role } = loadCompiled('service/role/role')
|
|
11
|
-
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
12
|
-
const { User } = loadCompiled('service/user/user')
|
|
13
|
-
const { AccessRequest } = loadCompiled('service/access-request/access-request')
|
|
14
|
-
const access = loadCompiled('controllers/access-request')
|
|
15
|
-
|
|
16
|
-
const onPostgres = process.env.AUTH_TEST_PG ? describe : describe.skip
|
|
17
|
-
|
|
18
|
-
onPostgres('on postgres', () => {
|
|
19
|
-
let domain: any
|
|
20
|
-
let kim: any
|
|
21
|
-
let park: any
|
|
22
|
-
let owner: any
|
|
23
|
-
let inbound: any
|
|
24
|
-
|
|
25
|
-
const inTransaction = <T>(user: any, run: (context: any) => Promise<T>) =>
|
|
26
|
-
authDataSource().transaction(tx => run({ state: { domain, user, tx, afterCommit: [] }, t: (key: string) => key }))
|
|
27
|
-
|
|
28
|
-
beforeAll(async () => {
|
|
29
|
-
await openAuthDatabase()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
afterAll(async () => {
|
|
33
|
-
await closeAuthDatabase()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
beforeEach(async () => {
|
|
37
|
-
await resetAuthDatabase()
|
|
38
|
-
domain = await getRepository(Domain).findOneBy({ id: (await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })).id })
|
|
39
|
-
const mutation = await getRepository(Privilege).save({ name: 'mutation', category: 'warehouse' })
|
|
40
|
-
const grant = await getRepository(Privilege).save({ name: 'mutation', category: 'user' })
|
|
41
|
-
inbound = await getRepository(Role).save({ name: 'inbound', domain, privileges: [mutation] })
|
|
42
|
-
const admins = await getRepository(Role).save({ name: 'admins', domain, privileges: [grant] })
|
|
43
|
-
const make = (name: string, roles: any[] = []) =>
|
|
44
|
-
getRepository(User).save({ username: name, email: `${name}@example.com`, name, salt: User.generateSalt(), status: 'activated', domains: [domain], roles })
|
|
45
|
-
kim = await make('kim')
|
|
46
|
-
park = await make('park', [admins])
|
|
47
|
-
owner = await make('owner', [admins])
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('★ two people deciding at once, each in a transaction - exactly one wins, and the loser leaves nothing', async () => {
|
|
51
|
-
const request = await inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context))
|
|
52
|
-
|
|
53
|
-
const results = await Promise.allSettled([
|
|
54
|
-
inTransaction(owner, context => access.decideAccessRequest({ requestId: request.id, decision: 'DENY', reason: 'no' }, context)),
|
|
55
|
-
inTransaction(park, context => access.decideAccessRequest({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, context))
|
|
56
|
-
])
|
|
57
|
-
|
|
58
|
-
expect(results.map(r => r.status).sort()).toEqual(['fulfilled', 'rejected'])
|
|
59
|
-
const stored = await getRepository(AccessRequest).findOne({ where: { id: request.id }, relations: ['grantedRole'] })
|
|
60
|
-
const holds = await User.hasPrivilege('mutation', 'warehouse', domain, kim)
|
|
61
|
-
/* The role is given exactly when the stored decision is a grant - a losing grant does not leave the role behind. */
|
|
62
|
-
expect(holds).toBe(stored.state === 'GRANTED')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('★ a grant closes the requester\'s other requests it answers - read inside the transaction, not the default connection', async () => {
|
|
66
|
-
const query = await getRepository(Privilege).save({ name: 'query', category: 'warehouse' })
|
|
67
|
-
const both = await getRepository(Role).save({ name: 'both', domain, privileges: [query, await getRepository(Privilege).findOneBy({ name: 'mutation', category: 'warehouse' })] })
|
|
68
|
-
const first = await inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context))
|
|
69
|
-
const second = await inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'query', reason: 'stock' }, context))
|
|
70
|
-
|
|
71
|
-
await inTransaction(owner, context => access.decideAccessRequest({ requestId: first.id, decision: 'GRANT', roleId: both.id }, context))
|
|
72
|
-
|
|
73
|
-
expect((await getRepository(AccessRequest).findOneBy({ id: second.id })).state).toBe('GRANTED')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('★ the same person asking twice at once - one pending request, not two', async () => {
|
|
77
|
-
await Promise.allSettled([
|
|
78
|
-
inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context)),
|
|
79
|
-
inTransaction(kim, context => access.requestAccess({ category: 'warehouse', privilege: 'mutation', reason: 'inbound' }, context))
|
|
80
|
-
])
|
|
81
|
-
expect(await getRepository(AccessRequest).count({ where: { state: 'PENDING' } })).toBe(1)
|
|
82
|
-
})
|
|
83
|
-
})
|