@things-factory/auth-base 10.0.0 → 10.0.3
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/dist-server/index.d.ts +3 -0
- package/dist-server/index.js +3 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/middlewares/domain-authenticate-middleware.js +26 -29
- package/dist-server/middlewares/domain-authenticate-middleware.js.map +1 -1
- package/dist-server/service/domain-generator/domain-generator-mutation.js +8 -4
- package/dist-server/service/domain-generator/domain-generator-mutation.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/user/user-mutation.js +9 -5
- package/dist-server/service/user/user-mutation.js.map +1 -1
- package/dist-server/service/user/user-query.js +4 -1
- package/dist-server/service/user/user-query.js.map +1 -1
- package/dist-server/service/user/user.d.ts +12 -0
- package/dist-server/service/user/user.js +22 -9
- package/dist-server/service/user/user.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/can-user-checkin-domain.d.ts +15 -0
- package/dist-server/utils/can-user-checkin-domain.js +28 -0
- package/dist-server/utils/can-user-checkin-domain.js.map +1 -0
- package/dist-server/utils/checkin-domain-facts.d.ts +37 -0
- package/dist-server/utils/checkin-domain-facts.js +218 -0
- package/dist-server/utils/checkin-domain-facts.js.map +1 -0
- package/dist-server/utils/checkin-domain-rule.d.ts +72 -0
- package/dist-server/utils/checkin-domain-rule.js +76 -0
- package/dist-server/utils/checkin-domain-rule.js.map +1 -0
- package/dist-server/utils/domain-ownership.d.ts +36 -0
- package/dist-server/utils/domain-ownership.js +64 -0
- package/dist-server/utils/domain-ownership.js.map +1 -0
- package/dist-server/utils/get-user-domains.d.ts +16 -0
- package/dist-server/utils/get-user-domains.js +47 -89
- package/dist-server/utils/get-user-domains.js.map +1 -1
- package/package.json +4 -4
- package/spec/unit/checkin-domain.spec.ts +307 -0
- package/spec/unit/checkin-privilege.spec.ts +205 -0
- package/spec/unit/domain-ownership.spec.ts +188 -0
- package/tests/checkin-domain-rule.test.ts +128 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { Domain } from '@things-factory/shell';
|
|
2
2
|
import { User } from '../service/user/user.js';
|
|
3
|
+
/**
|
|
4
|
+
* 사용자가 체크인할 수 있는 도메인 목록.
|
|
5
|
+
*
|
|
6
|
+
* 판정 규칙과 그 근거는 `checkin-domain-rule.ts` 에 있다. 요약하면 체크인 대상은
|
|
7
|
+
* "역할을 정의한 도메인" 이 아니라 "역할을 부여한 도메인" 이며, 부모 도메인으로부터 상속받은
|
|
8
|
+
* 역할을 부여한 경우에도 부여가 일어난 자식 도메인이 대상이다.
|
|
9
|
+
*
|
|
10
|
+
* 목록이 필요한 지점(체크인 화면·부트스트랩)에서만 사용한다. 도메인 하나에 대한 가부만
|
|
11
|
+
* 필요하면 `canUserCheckinDomain` 을 쓴다.
|
|
12
|
+
*/
|
|
3
13
|
export declare function getUserDomains(user: User): Promise<Partial<Domain>[]>;
|
|
14
|
+
/**
|
|
15
|
+
* 체크인 가능한 도메인 중, 체크인 근거가 된 역할이 주어진 권한을 가진 도메인 목록.
|
|
16
|
+
*
|
|
17
|
+
* 상속 역할로 체크인하는 자식 도메인은 부모 도메인에 정의된 역할의 권한으로 판정한다.
|
|
18
|
+
* 도메인 소유자는 권한 판정 없이 포함한다 (기존 동작 유지).
|
|
19
|
+
*/
|
|
4
20
|
export declare function getDomainsWithPrivilege(user: User, privilege: string, category: string): Promise<Partial<Domain>[]>;
|
|
5
21
|
export declare function getDomainsAsOwner(user: User): Promise<Partial<Domain>[]>;
|
|
@@ -5,102 +5,60 @@ exports.getDomainsWithPrivilege = getDomainsWithPrivilege;
|
|
|
5
5
|
exports.getDomainsAsOwner = getDomainsAsOwner;
|
|
6
6
|
const shell_1 = require("@things-factory/shell");
|
|
7
7
|
const env_1 = require("@things-factory/env");
|
|
8
|
-
const
|
|
8
|
+
const checkin_domain_facts_js_1 = require("./checkin-domain-facts.js");
|
|
9
|
+
const checkin_domain_rule_js_1 = require("./checkin-domain-rule.js");
|
|
9
10
|
const domainTypes = env_1.config.get('domainTypes', []);
|
|
11
|
+
function byName(left, right) {
|
|
12
|
+
return (left.name || '').localeCompare(right.name || '');
|
|
13
|
+
}
|
|
14
|
+
function brief(domain) {
|
|
15
|
+
const { id, name, description, subdomain, extType, brandName, brandImage } = domain;
|
|
16
|
+
/* do not remove id column for result */
|
|
17
|
+
return { id, name, description, subdomain, extType, brandName, brandImage };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 사용자가 체크인할 수 있는 도메인 목록.
|
|
21
|
+
*
|
|
22
|
+
* 판정 규칙과 그 근거는 `checkin-domain-rule.ts` 에 있다. 요약하면 체크인 대상은
|
|
23
|
+
* "역할을 정의한 도메인" 이 아니라 "역할을 부여한 도메인" 이며, 부모 도메인으로부터 상속받은
|
|
24
|
+
* 역할을 부여한 경우에도 부여가 일어난 자식 도메인이 대상이다.
|
|
25
|
+
*
|
|
26
|
+
* 목록이 필요한 지점(체크인 화면·부트스트랩)에서만 사용한다. 도메인 하나에 대한 가부만
|
|
27
|
+
* 필요하면 `canUserCheckinDomain` 을 쓴다.
|
|
28
|
+
*/
|
|
10
29
|
async function getUserDomains(user) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.leftJoin('ROLE.domain', 'DOMAIN')
|
|
21
|
-
.where('USER.id = :userId', { userId: user.id })
|
|
22
|
-
.getQuery();
|
|
23
|
-
return 'DOMAIN.id IN ' + subQuery;
|
|
24
|
-
})
|
|
25
|
-
.orWhere(qb => {
|
|
26
|
-
const subQuery = qb
|
|
27
|
-
.subQuery()
|
|
28
|
-
.select('DOMAIN.id')
|
|
29
|
-
.from(shell_1.Domain, 'DOMAIN')
|
|
30
|
-
.where('DOMAIN.owner = :owner', { owner: user.id })
|
|
31
|
-
.getQuery();
|
|
32
|
-
return 'DOMAIN.id IN ' + subQuery;
|
|
33
|
-
})
|
|
34
|
-
.orderBy('DOMAIN.name', 'ASC')
|
|
35
|
-
.getMany())
|
|
36
|
-
.filter(d => !d.extType || domainTypes.includes(d.extType))
|
|
37
|
-
.map(domain => {
|
|
38
|
-
const { id, name, description, subdomain, extType, brandName, brandImage } = domain;
|
|
39
|
-
/* do not remove id column for result */
|
|
40
|
-
return { id, name, description, subdomain, extType, brandName, brandImage };
|
|
41
|
-
});
|
|
30
|
+
if (!user?.id) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const { facts, candidates } = await (0, checkin_domain_facts_js_1.collectCheckinCandidates)(user);
|
|
34
|
+
return candidates
|
|
35
|
+
.filter(domain => (0, checkin_domain_rule_js_1.checkinGrantsOf)(domain, facts).length > 0)
|
|
36
|
+
.filter(domain => !domain.extType || domainTypes.includes(domain.extType))
|
|
37
|
+
.sort(byName)
|
|
38
|
+
.map(brief);
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// .subQuery()
|
|
50
|
-
// .distinct(true)
|
|
51
|
-
// .select('DOMAIN.id')
|
|
52
|
-
// .from(User, 'USER')
|
|
53
|
-
// .leftJoin('USER.roles', 'ROLE')
|
|
54
|
-
// .leftJoin('ROLE.domain', 'DOMAIN')
|
|
55
|
-
// .where('USER.id = :userId', { userId: user.id })
|
|
56
|
-
// .getQuery()
|
|
57
|
-
// return 'DOMAIN.id IN ' + subQuery
|
|
58
|
-
// })
|
|
59
|
-
// .getMany()
|
|
60
|
-
// ).map(domain => {
|
|
61
|
-
// const { id, name, description, subdomain, extType, brandName, brandImage } = domain
|
|
62
|
-
// return { id, name, description, subdomain, extType, brandName, brandImage }
|
|
63
|
-
// })
|
|
64
|
-
// }
|
|
40
|
+
/**
|
|
41
|
+
* 체크인 가능한 도메인 중, 체크인 근거가 된 역할이 주어진 권한을 가진 도메인 목록.
|
|
42
|
+
*
|
|
43
|
+
* 상속 역할로 체크인하는 자식 도메인은 부모 도메인에 정의된 역할의 권한으로 판정한다.
|
|
44
|
+
* 도메인 소유자는 권한 판정 없이 포함한다 (기존 동작 유지).
|
|
45
|
+
*/
|
|
65
46
|
async function getDomainsWithPrivilege(user, privilege, category) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.where('USER.id = :userId', { userId: user.id })
|
|
78
|
-
.andWhere('PRIVILEGE.name = :privilege', { privilege })
|
|
79
|
-
.andWhere('PRIVILEGE.category = :category', { category })
|
|
80
|
-
.getQuery();
|
|
81
|
-
return 'DOMAIN.id IN ' + subQuery;
|
|
82
|
-
})
|
|
83
|
-
.orWhere(qb => {
|
|
84
|
-
const subQuery = qb
|
|
85
|
-
.subQuery()
|
|
86
|
-
.select('DOMAIN.id')
|
|
87
|
-
.from(shell_1.Domain, 'DOMAIN')
|
|
88
|
-
.where('DOMAIN.owner = :owner', { owner: user.id })
|
|
89
|
-
.getQuery();
|
|
90
|
-
return 'DOMAIN.id IN ' + subQuery;
|
|
91
|
-
})
|
|
92
|
-
.orderBy('DOMAIN.name', 'ASC')
|
|
93
|
-
.getMany()).map(domain => {
|
|
94
|
-
const { id, name, description, subdomain, extType, brandName, brandImage } = domain;
|
|
95
|
-
return { id, name, description, subdomain, extType, brandName, brandImage };
|
|
96
|
-
});
|
|
47
|
+
if (!user?.id) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const [{ facts, candidates }, privilegedRoleDomainIds] = await Promise.all([
|
|
51
|
+
(0, checkin_domain_facts_js_1.collectCheckinCandidates)(user),
|
|
52
|
+
(0, checkin_domain_facts_js_1.findRoleDomainIdsWithPrivilege)(user, privilege, category)
|
|
53
|
+
]);
|
|
54
|
+
return candidates
|
|
55
|
+
.filter(domain => (0, checkin_domain_rule_js_1.checkinGrantsOf)(domain, facts).some(grant => grant.via === 'owner' || privilegedRoleDomainIds.has(grant.roleDomainId)))
|
|
56
|
+
.sort(byName)
|
|
57
|
+
.map(brief);
|
|
97
58
|
}
|
|
98
59
|
async function getDomainsAsOwner(user) {
|
|
99
60
|
return (await (0, shell_1.getRepository)(shell_1.Domain).find({
|
|
100
61
|
where: { owner: user.id }
|
|
101
|
-
})).map(
|
|
102
|
-
const { id, name, description, subdomain, extType, brandName, brandImage } = domain;
|
|
103
|
-
return { id, name, description, subdomain, extType, brandName, brandImage };
|
|
104
|
-
});
|
|
62
|
+
})).map(brief);
|
|
105
63
|
}
|
|
106
64
|
//# sourceMappingURL=get-user-domains.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-user-domains.js","sourceRoot":"","sources":["../../server/utils/get-user-domains.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"get-user-domains.js","sourceRoot":"","sources":["../../server/utils/get-user-domains.ts"],"names":[],"mappings":";;AA6BA,wCAYC;AAQD,0DAsBC;AAED,8CAMC;AA/ED,iDAA6D;AAC7D,6CAA4C;AAG5C,uEAAoG;AACpG,qEAA0D;AAE1D,MAAM,WAAW,GAAG,YAAM,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;AAEjD,SAAS,MAAM,CAAC,IAAY,EAAE,KAAa;IACzC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;AAC1D,CAAC;AAED,SAAS,KAAK,CAAC,MAAc;IAC3B,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,MAAM,CAAA;IACnF,wCAAwC;IACxC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAA;AAC7E,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,cAAc,CAAC,IAAU;IAC7C,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;QACd,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,IAAA,kDAAwB,EAAC,IAAI,CAAC,CAAA;IAElE,OAAO,UAAU;SACd,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,IAAA,wCAAe,EAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3D,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;SACzE,IAAI,CAAC,MAAM,CAAC;SACZ,GAAG,CAAC,KAAK,CAAC,CAAA;AACf,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,uBAAuB,CAC3C,IAAU,EACV,SAAiB,EACjB,QAAgB;IAEhB,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;QACd,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,uBAAuB,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACzE,IAAA,kDAAwB,EAAC,IAAI,CAAC;QAC9B,IAAA,wDAA8B,EAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC;KAC1D,CAAC,CAAA;IAEF,OAAO,UAAU;SACd,MAAM,CAAC,MAAM,CAAC,EAAE,CACf,IAAA,wCAAe,EAAC,MAAM,EAAE,KAAK,CAAC,CAAC,IAAI,CACjC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAClF,CACF;SACA,IAAI,CAAC,MAAM,CAAC;SACZ,GAAG,CAAC,KAAK,CAAC,CAAA;AACf,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,IAAU;IAChD,OAAO,CACL,MAAM,IAAA,qBAAa,EAAC,cAAM,CAAC,CAAC,IAAI,CAAC;QAC/B,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE;KAC1B,CAAC,CACH,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;AACd,CAAC","sourcesContent":["import { Domain, getRepository } from '@things-factory/shell'\nimport { config } from '@things-factory/env'\n\nimport { User } from '../service/user/user.js'\nimport { collectCheckinCandidates, findRoleDomainIdsWithPrivilege } from './checkin-domain-facts.js'\nimport { checkinGrantsOf } from './checkin-domain-rule.js'\n\nconst domainTypes = config.get('domainTypes', [])\n\nfunction byName(left: Domain, right: Domain): number {\n return (left.name || '').localeCompare(right.name || '')\n}\n\nfunction brief(domain: Domain): Partial<Domain> {\n const { id, name, description, subdomain, extType, brandName, brandImage } = domain\n /* do not remove id column for result */\n return { id, name, description, subdomain, extType, brandName, brandImage }\n}\n\n/**\n * 사용자가 체크인할 수 있는 도메인 목록.\n *\n * 판정 규칙과 그 근거는 `checkin-domain-rule.ts` 에 있다. 요약하면 체크인 대상은\n * \"역할을 정의한 도메인\" 이 아니라 \"역할을 부여한 도메인\" 이며, 부모 도메인으로부터 상속받은\n * 역할을 부여한 경우에도 부여가 일어난 자식 도메인이 대상이다.\n *\n * 목록이 필요한 지점(체크인 화면·부트스트랩)에서만 사용한다. 도메인 하나에 대한 가부만\n * 필요하면 `canUserCheckinDomain` 을 쓴다.\n */\nexport async function getUserDomains(user: User): Promise<Partial<Domain>[]> {\n if (!user?.id) {\n return []\n }\n\n const { facts, candidates } = await collectCheckinCandidates(user)\n\n return candidates\n .filter(domain => checkinGrantsOf(domain, facts).length > 0)\n .filter(domain => !domain.extType || domainTypes.includes(domain.extType))\n .sort(byName)\n .map(brief)\n}\n\n/**\n * 체크인 가능한 도메인 중, 체크인 근거가 된 역할이 주어진 권한을 가진 도메인 목록.\n *\n * 상속 역할로 체크인하는 자식 도메인은 부모 도메인에 정의된 역할의 권한으로 판정한다.\n * 도메인 소유자는 권한 판정 없이 포함한다 (기존 동작 유지).\n */\nexport async function getDomainsWithPrivilege(\n user: User,\n privilege: string,\n category: string\n): Promise<Partial<Domain>[]> {\n if (!user?.id) {\n return []\n }\n\n const [{ facts, candidates }, privilegedRoleDomainIds] = await Promise.all([\n collectCheckinCandidates(user),\n findRoleDomainIdsWithPrivilege(user, privilege, category)\n ])\n\n return candidates\n .filter(domain =>\n checkinGrantsOf(domain, facts).some(\n grant => grant.via === 'owner' || privilegedRoleDomainIds.has(grant.roleDomainId)\n )\n )\n .sort(byName)\n .map(brief)\n}\n\nexport async function getDomainsAsOwner(user: User): Promise<Partial<Domain>[]> {\n return (\n await getRepository(Domain).find({\n where: { owner: user.id }\n })\n ).map(brief)\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/auth-base",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.3",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"@reduxjs/toolkit": "^2.2.5",
|
|
35
35
|
"@simplewebauthn/browser": "^13.0.0",
|
|
36
36
|
"@simplewebauthn/server": "^13.0.0",
|
|
37
|
-
"@things-factory/email-base": "^10.0.
|
|
37
|
+
"@things-factory/email-base": "^10.0.3",
|
|
38
38
|
"@things-factory/env": "^10.0.0",
|
|
39
|
-
"@things-factory/shell": "^10.0.
|
|
39
|
+
"@things-factory/shell": "^10.0.3",
|
|
40
40
|
"@things-factory/utils": "^10.0.0",
|
|
41
41
|
"@types/webappsec-credential-management": "^0.6.9",
|
|
42
42
|
"jsonwebtoken": "^9.0.0",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"passport-jwt": "^4.0.0",
|
|
49
49
|
"passport-local": "^1.0.0"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "c7d253b722b6917e8911f6ef6e78e5fc2b295e82"
|
|
52
52
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 체크인 자격 판정 — 실제 엔티티·실제 쿼리 검증.
|
|
3
|
+
*
|
|
4
|
+
* 보안 판정이므로 로직을 재현하지 않고, 빌드된 `@things-factory/auth-base` 의 실제 함수
|
|
5
|
+
* (`getUserDomains` / `canUserCheckinDomain`)를 실제 엔티티 메타데이터(실제 조인 테이블·네이밍
|
|
6
|
+
* 전략)로 만든 sqlite in-memory 스키마에 대해 그대로 실행한다.
|
|
7
|
+
*
|
|
8
|
+
* 주의: 이 스펙은 빌드 산출물(`dist-server`)을 대상으로 한다 — 소스를 고친 뒤에는
|
|
9
|
+
* `npm run build:server` 를 먼저 실행해야 변경이 반영된다.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { DataSource } from 'typeorm'
|
|
13
|
+
|
|
14
|
+
import { NamingStrategy, addDataSource, entities as shellEntities, Domain } from '@things-factory/shell'
|
|
15
|
+
import {
|
|
16
|
+
DomainOwner,
|
|
17
|
+
GrantedRole,
|
|
18
|
+
Role,
|
|
19
|
+
User,
|
|
20
|
+
canUserCheckinDomain,
|
|
21
|
+
entities as authEntities,
|
|
22
|
+
getUserDomains
|
|
23
|
+
} from '@things-factory/auth-base'
|
|
24
|
+
|
|
25
|
+
let dataSource: DataSource
|
|
26
|
+
|
|
27
|
+
async function domain(name: string, parent?: Domain, owner?: string): Promise<Domain> {
|
|
28
|
+
return await dataSource.getRepository(Domain).save({
|
|
29
|
+
name,
|
|
30
|
+
subdomain: name,
|
|
31
|
+
parent,
|
|
32
|
+
owner
|
|
33
|
+
} as any)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function user(name: string, domains: Domain[], roles: Role[] = []): Promise<User> {
|
|
37
|
+
return await dataSource.getRepository(User).save({
|
|
38
|
+
name,
|
|
39
|
+
email: `${name}@test.com`,
|
|
40
|
+
username: name,
|
|
41
|
+
domains,
|
|
42
|
+
roles
|
|
43
|
+
} as any)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function role(name: string, roleDomain: Domain): Promise<Role> {
|
|
47
|
+
return await dataSource.getRepository(Role).save({ name, domain: roleDomain } as any)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 판정에 쓰이는 도메인은 `context.state.domain` 처럼 온전히 적재된 것이어야 한다. */
|
|
51
|
+
async function reload(target: Domain): Promise<Domain> {
|
|
52
|
+
return await dataSource.getRepository(Domain).findOneBy({ id: target.id })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function reloadUser(target: User): Promise<User> {
|
|
56
|
+
return await dataSource.getRepository(User).findOne({ where: { id: target.id }, relations: ['domains'] })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function checkinSubdomains(target: User): Promise<string[]> {
|
|
60
|
+
return (await getUserDomains(await reloadUser(target))).map(d => d.subdomain)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
dataSource = new DataSource({
|
|
65
|
+
type: 'sqlite',
|
|
66
|
+
database: ':memory:',
|
|
67
|
+
synchronize: true,
|
|
68
|
+
dropSchema: true,
|
|
69
|
+
namingStrategy: new NamingStrategy(),
|
|
70
|
+
entities: [...shellEntities, ...authEntities],
|
|
71
|
+
logging: false
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await dataSource.initialize()
|
|
75
|
+
addDataSource('default', dataSource)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
afterAll(async () => {
|
|
79
|
+
await dataSource?.destroy()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
beforeEach(async () => {
|
|
83
|
+
/* 스펙 간 격리 — 테이블 이름은 엔티티 메타데이터에서 얻는다 (네이밍 전략에 의존하지 않기 위해). */
|
|
84
|
+
for (const junction of dataSource.entityMetadatas.filter(metadata => metadata.isJunction)) {
|
|
85
|
+
await dataSource.query(`DELETE FROM "${junction.tableName}"`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const target of [GrantedRole, DomainOwner, Role, User, Domain]) {
|
|
89
|
+
await dataSource.query(`DELETE FROM "${dataSource.getMetadata(target).tableName}"`)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('부모 도메인으로부터 상속받은 역할', () => {
|
|
94
|
+
it('부여가 일어난 자식 도메인에 체크인할 수 있다', async () => {
|
|
95
|
+
const parent = await domain('parent')
|
|
96
|
+
const child = await domain('child', parent)
|
|
97
|
+
const inheritedRole = await role('operator', parent)
|
|
98
|
+
const member = await user('kim', [child], [inheritedRole])
|
|
99
|
+
|
|
100
|
+
expect(await canUserCheckinDomain(await reloadUser(member), await reload(child))).toBe(true)
|
|
101
|
+
expect(await checkinSubdomains(member)).toEqual(['child'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('역할을 정의한 부모 도메인에는 체크인할 수 없다', async () => {
|
|
105
|
+
const parent = await domain('parent')
|
|
106
|
+
const child = await domain('child', parent)
|
|
107
|
+
const inheritedRole = await role('operator', parent)
|
|
108
|
+
const member = await user('kim', [child], [inheritedRole])
|
|
109
|
+
|
|
110
|
+
expect(await canUserCheckinDomain(await reloadUser(member), await reload(parent))).toBe(false)
|
|
111
|
+
expect(await checkinSubdomains(member)).not.toContain('parent')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('멤버가 아닌 형제 자식 도메인에는 체크인할 수 없다', async () => {
|
|
115
|
+
const parent = await domain('parent')
|
|
116
|
+
const child = await domain('child', parent)
|
|
117
|
+
const sibling = await domain('sibling', parent)
|
|
118
|
+
const inheritedRole = await role('operator', parent)
|
|
119
|
+
const member = await user('kim', [child], [inheritedRole])
|
|
120
|
+
|
|
121
|
+
expect(await canUserCheckinDomain(await reloadUser(member), await reload(sibling))).toBe(false)
|
|
122
|
+
expect(await checkinSubdomains(member)).toEqual(['child'])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('부모의 역할을 부모에서 부여받았다면 부모에 체크인한다', async () => {
|
|
126
|
+
const parent = await domain('parent')
|
|
127
|
+
const child = await domain('child', parent)
|
|
128
|
+
const parentRole = await role('operator', parent)
|
|
129
|
+
const member = await user('kim', [parent], [parentRole])
|
|
130
|
+
|
|
131
|
+
expect(await canUserCheckinDomain(await reloadUser(member), await reload(parent))).toBe(true)
|
|
132
|
+
expect(await canUserCheckinDomain(await reloadUser(member), await reload(child))).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('자기 도메인에 정의된 역할', () => {
|
|
137
|
+
it('멤버이고 역할이 있으면 체크인한다', async () => {
|
|
138
|
+
const target = await domain('acme')
|
|
139
|
+
const ownRole = await role('operator', target)
|
|
140
|
+
const member = await user('kim', [target], [ownRole])
|
|
141
|
+
|
|
142
|
+
expect(await canUserCheckinDomain(await reloadUser(member), await reload(target))).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('멤버이지만 역할이 없으면 체크인하지 못한다', async () => {
|
|
146
|
+
const target = await domain('acme')
|
|
147
|
+
const member = await user('kim', [target], [])
|
|
148
|
+
|
|
149
|
+
expect(await canUserCheckinDomain(await reloadUser(member), await reload(target))).toBe(false)
|
|
150
|
+
expect(await checkinSubdomains(member)).toEqual([])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('역할이 있지만 멤버가 아니면 체크인하지 못한다', async () => {
|
|
154
|
+
const target = await domain('acme')
|
|
155
|
+
const outsideRole = await role('operator', target)
|
|
156
|
+
const outsider = await user('lee', [], [outsideRole])
|
|
157
|
+
|
|
158
|
+
expect(await canUserCheckinDomain(await reloadUser(outsider), await reload(target))).toBe(false)
|
|
159
|
+
expect(await checkinSubdomains(outsider)).toEqual([])
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('파트너십으로 grant 된 역할', () => {
|
|
164
|
+
it('공급자 도메인의 멤버가 아니어도 체크인한다', async () => {
|
|
165
|
+
const supplier = await domain('supplier')
|
|
166
|
+
const customer = await domain('customer')
|
|
167
|
+
const supplierRole = await role('carrier', supplier)
|
|
168
|
+
await dataSource.getRepository(GrantedRole).save({ role: supplierRole, domain: customer } as any)
|
|
169
|
+
|
|
170
|
+
const customerUser = await user('park', [customer], [supplierRole])
|
|
171
|
+
|
|
172
|
+
expect(await canUserCheckinDomain(await reloadUser(customerUser), await reload(supplier))).toBe(true)
|
|
173
|
+
expect((await checkinSubdomains(customerUser)).sort()).toEqual(['supplier'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('grant 되지 않은 역할은 파트너 경로를 열지 않는다', async () => {
|
|
177
|
+
const supplier = await domain('supplier')
|
|
178
|
+
const customer = await domain('customer')
|
|
179
|
+
const supplierRole = await role('carrier', supplier)
|
|
180
|
+
|
|
181
|
+
const customerUser = await user('park', [customer], [supplierRole])
|
|
182
|
+
|
|
183
|
+
expect(await canUserCheckinDomain(await reloadUser(customerUser), await reload(supplier))).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('다른 도메인에 grant 된 역할은 근거가 되지 않는다', async () => {
|
|
187
|
+
const supplier = await domain('supplier')
|
|
188
|
+
const customer = await domain('customer')
|
|
189
|
+
const stranger = await domain('stranger')
|
|
190
|
+
const supplierRole = await role('carrier', supplier)
|
|
191
|
+
await dataSource.getRepository(GrantedRole).save({ role: supplierRole, domain: stranger } as any)
|
|
192
|
+
|
|
193
|
+
const customerUser = await user('park', [customer], [supplierRole])
|
|
194
|
+
|
|
195
|
+
expect(await canUserCheckinDomain(await reloadUser(customerUser), await reload(supplier))).toBe(false)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('소유권', () => {
|
|
200
|
+
it('하위 호환 owner 컬럼으로 체크인한다', async () => {
|
|
201
|
+
const target = await domain('acme')
|
|
202
|
+
const owner = await user('boss', [])
|
|
203
|
+
await dataSource.getRepository(Domain).update(target.id, { owner: owner.id })
|
|
204
|
+
|
|
205
|
+
expect(await canUserCheckinDomain(await reloadUser(owner), await reload(target))).toBe(true)
|
|
206
|
+
expect(await checkinSubdomains(owner)).toEqual(['acme'])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('DomainOwner 테이블 기준으로도 체크인한다', async () => {
|
|
210
|
+
const target = await domain('acme')
|
|
211
|
+
const owner = await user('boss', [])
|
|
212
|
+
await dataSource.getRepository(DomainOwner).save({ domain: target, user: owner } as any)
|
|
213
|
+
|
|
214
|
+
expect(await canUserCheckinDomain(await reloadUser(owner), await reload(target))).toBe(true)
|
|
215
|
+
expect(await checkinSubdomains(owner)).toEqual(['acme'])
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('다른 사용자가 owner 인 도메인은 체크인하지 못한다', async () => {
|
|
219
|
+
const target = await domain('acme')
|
|
220
|
+
const owner = await user('boss', [])
|
|
221
|
+
const other = await user('lee', [])
|
|
222
|
+
await dataSource.getRepository(Domain).update(target.id, { owner: owner.id })
|
|
223
|
+
|
|
224
|
+
expect(await canUserCheckinDomain(await reloadUser(other), await reload(target))).toBe(false)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('핫패스 조회 비용', () => {
|
|
229
|
+
/**
|
|
230
|
+
* `canUserCheckinDomain` 은 매 요청 도메인 인증에서 호출된다. 도메인 목록 전체를 계산하지
|
|
231
|
+
* 않고 대상 도메인만 판정하도록 되어 있는데, 이 성질이 깨지면 요청당 비용이 늘어난다.
|
|
232
|
+
*/
|
|
233
|
+
async function countQueries(work: () => Promise<unknown>): Promise<number> {
|
|
234
|
+
let count = 0
|
|
235
|
+
const original = (dataSource as any).logger
|
|
236
|
+
|
|
237
|
+
;(dataSource as any).logger = {
|
|
238
|
+
logQuery: () => {
|
|
239
|
+
count++
|
|
240
|
+
},
|
|
241
|
+
logQueryError: () => {},
|
|
242
|
+
logQuerySlow: () => {},
|
|
243
|
+
logSchemaBuild: () => {},
|
|
244
|
+
logMigration: () => {},
|
|
245
|
+
log: () => {}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await work()
|
|
250
|
+
} finally {
|
|
251
|
+
;(dataSource as any).logger = original
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return count
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
it('통과 판정은 1회 조회로 끝난다', async () => {
|
|
258
|
+
const parent = await domain('parent')
|
|
259
|
+
const child = await domain('child', parent)
|
|
260
|
+
const inheritedRole = await role('operator', parent)
|
|
261
|
+
const member = await user('kim', [child], [inheritedRole])
|
|
262
|
+
|
|
263
|
+
const loadedUser = await reloadUser(member)
|
|
264
|
+
const loadedDomain = await reload(child)
|
|
265
|
+
|
|
266
|
+
expect(await countQueries(() => canUserCheckinDomain(loadedUser, loadedDomain))).toBe(1)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('목록 계산은 4회 조회로 끝난다 (경로별 조회가 도메인 행을 직접 반환한다)', async () => {
|
|
270
|
+
const parent = await domain('parent')
|
|
271
|
+
const child = await domain('child', parent)
|
|
272
|
+
const inheritedRole = await role('operator', parent)
|
|
273
|
+
const member = await user('kim', [child], [inheritedRole])
|
|
274
|
+
|
|
275
|
+
const loadedUser = await reloadUser(member)
|
|
276
|
+
|
|
277
|
+
expect(await countQueries(() => getUserDomains(loadedUser))).toBe(4)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('거절 판정도 3회를 넘지 않는다', async () => {
|
|
281
|
+
const target = await domain('acme')
|
|
282
|
+
const outsider = await user('lee', [], [])
|
|
283
|
+
|
|
284
|
+
const loadedUser = await reloadUser(outsider)
|
|
285
|
+
const loadedDomain = await reload(target)
|
|
286
|
+
|
|
287
|
+
expect(await countQueries(() => canUserCheckinDomain(loadedUser, loadedDomain))).toBeLessThanOrEqual(3)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('여러 도메인이 얽힌 목록 계산', () => {
|
|
292
|
+
it('부여받은 도메인만 목록에 담는다', async () => {
|
|
293
|
+
const parent = await domain('parent')
|
|
294
|
+
const childA = await domain('child-a', parent)
|
|
295
|
+
const childB = await domain('child-b', parent)
|
|
296
|
+
const other = await domain('other')
|
|
297
|
+
|
|
298
|
+
const inheritedRole = await role('operator', parent)
|
|
299
|
+
const childBRole = await role('viewer', childB)
|
|
300
|
+
await role('unassigned', other)
|
|
301
|
+
|
|
302
|
+
/* childA: 상속 역할 부여 / childB: 자기 역할 부여 / other: 멤버이지만 역할 없음 */
|
|
303
|
+
const member = await user('kim', [childA, childB, other], [inheritedRole, childBRole])
|
|
304
|
+
|
|
305
|
+
expect((await checkinSubdomains(member)).sort()).toEqual(['child-a', 'child-b'])
|
|
306
|
+
})
|
|
307
|
+
})
|