@things-factory/auth-base 10.0.7 → 10.0.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-base",
3
- "version": "10.0.7",
3
+ "version": "10.0.9",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -34,10 +34,10 @@
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.7",
38
- "@things-factory/env": "^10.0.0",
39
- "@things-factory/shell": "^10.0.7",
40
- "@things-factory/utils": "^10.0.0",
37
+ "@things-factory/email-base": "^10.0.9",
38
+ "@things-factory/env": "^10.0.8",
39
+ "@things-factory/shell": "^10.0.9",
40
+ "@things-factory/utils": "^10.0.8",
41
41
  "@types/webappsec-credential-management": "^0.6.9",
42
42
  "jsonwebtoken": "^9.0.0",
43
43
  "koa-passport": "^6.0.0",
@@ -48,5 +48,5 @@
48
48
  "passport-jwt": "^4.0.0",
49
49
  "passport-local": "^1.0.0"
50
50
  },
51
- "gitHead": "fffd94e886b67ed9019993a572a632054e812fa3"
51
+ "gitHead": "b7a6c48c213e7a504114397b752004a295c1df72"
52
52
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 유령 권한 판정 테스트.
3
+ *
4
+ * 계기: 트윈에서 조회 게이트 둘(`twin/query`)을 걷어낸 뒤 DB 에 그 행이 남았다. 부팅 동기화는 없는 행을
5
+ * **추가만** 하고 사라진 지시자의 행을 지우지 않는다. 그래서 관리자는 역할에 붙일 수 있고 붙여도 아무
6
+ * 일이 없는 권한을 목록에서 보게 된다.
7
+ *
8
+ * 이 판정이 틀리는 방향은 두 가지이고, 둘의 값이 다르다:
9
+ * · 살아 있는 권한을 유령이라고 하면 → 관리자가 **필요한 권한을 뗀다**(사용자가 못 쓰게 된다).
10
+ * · 유령을 살아 있다고 하면 → 목록이 조금 지저분한 상태로 남는다(지금과 같다).
11
+ * 그래서 확신이 없을 때는 **유령이라고 말하지 않는다.**
12
+ */
13
+
14
+ import { declarationKey, isDeprecatedPrivilege } from '../server/service/privilege/privilege-deprecation.js'
15
+
16
+ /** 지시자가 실제로 만드는 모양: `process['PRIVILEGES']['twin mutation'] = ['twin', 'mutation']`. */
17
+ const declared = (...pairs: [string, string][]) =>
18
+ pairs.reduce((sum, [category, privilege]) => ({ ...sum, [declarationKey(category, privilege)]: [category, privilege] }), {})
19
+
20
+ const TWIN_MUTATION = { category: 'twin', name: 'mutation' }
21
+ const TWIN_QUERY = { category: 'twin', name: 'query' }
22
+
23
+ describe('privilege deprecation', () => {
24
+ it('선언된 권한은 유령이 아니다', () => {
25
+ const declarations = declared(['twin', 'mutation'], ['board', 'query'])
26
+
27
+ expect(isDeprecatedPrivilege(declarations, TWIN_MUTATION)).toBe(false)
28
+ })
29
+
30
+ it('선언이 사라진 권한은 유령이다 — 이 작업의 계기 그대로', () => {
31
+ /* 트윈 조회 게이트를 걷어낸 뒤의 상태: mutation 은 선언되어 있고 query 는 아니다. */
32
+ const declarations = declared(['twin', 'mutation'])
33
+
34
+ expect(isDeprecatedPrivilege(declarations, TWIN_QUERY)).toBe(true)
35
+ expect(isDeprecatedPrivilege(declarations, TWIN_MUTATION)).toBe(false)
36
+ })
37
+
38
+ it('category 와 name 을 함께 본다 — 한쪽만 같은 것을 짝으로 세지 않는다', () => {
39
+ const declarations = declared(['twin', 'mutation'])
40
+
41
+ /* 이름이 같아도 다른 category 면 다른 권한이다(`query`/`mutation` 은 43개 category 가 공유한다). */
42
+ expect(isDeprecatedPrivilege(declarations, { category: 'board', name: 'mutation' })).toBe(true)
43
+ expect(isDeprecatedPrivilege(declarations, { category: 'twin', name: 'query' })).toBe(true)
44
+ })
45
+
46
+ it('선언이 하나도 없으면 판단하지 않는다 — 전부 유령이라 답하지 않는다', () => {
47
+ /* 스키마가 아직 구성되지 않았거나 전역이 비어 있는 상황. 거짓 경보보다 무판단이 정직하다. */
48
+ for (const empty of [{}, undefined, null]) {
49
+ expect(isDeprecatedPrivilege(empty, TWIN_MUTATION)).toBe(false)
50
+ }
51
+ })
52
+
53
+ it('반쪽 행은 어떤 선언과도 짝지을 수 없다', () => {
54
+ const declarations = declared(['twin', 'mutation'])
55
+
56
+ expect(isDeprecatedPrivilege(declarations, { category: 'twin' })).toBe(true)
57
+ expect(isDeprecatedPrivilege(declarations, { name: 'mutation' })).toBe(true)
58
+ expect(isDeprecatedPrivilege(declarations, {})).toBe(true)
59
+ })
60
+
61
+ it('키 모양이 지시자와 같다 — 이 규약이 어긋나면 전부 유령이 된다', () => {
62
+ /* `privilege-directive.ts` 가 `${category} ${privilege}` 로 넣는다. 공백 하나가 규약이다. */
63
+ expect(declarationKey('twin', 'mutation')).toBe('twin mutation')
64
+ })
65
+ })
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 권한 거절 문장 테스트.
3
+ *
4
+ * 이 문장은 프레임워크 apollo 링크를 타고 **사용자 얼굴까지** 간다(자동으로 닫히지 않는 오류 토스트).
5
+ * 예전에는 영어 한 문장이었고, 나머지 서버 오류가 모두 `context.t` 로 옮겨진 뒤에도 여기만 남아 있었다.
6
+ *
7
+ * 위험한 것은 게이트 판정이 아니라 문장 만들기다:
8
+ * ① 번역기가 없는 컨텍스트(구독·내부 호출)에서 죽거나 빈 문장을 주면 사용자는 이유를 못 본다.
9
+ * ② i18next 는 키를 못 찾으면 **키를 그대로 돌려준다** — 그걸 쓰면 `error.x privilege required` 가 보인다.
10
+ * ③ 소비처가 문장 형태에 의존하면 번역하는 순간 깨진다 — 그래서 `extensions` 에 구조로 싣는다.
11
+ */
12
+
13
+ import { privilegeRejection } from '../server/service/privilege/privilege-rejection.js'
14
+
15
+ /** 키를 그대로 돌려주는 i18next 기본 거동(번역 누락). */
16
+ const missing = (key: string) => key
17
+
18
+ describe('privilege rejection', () => {
19
+ it('요청의 언어로 말한다', () => {
20
+ const context = { t: (key: string, params: any) => `${params.x} 권한이 필요합니다.` }
21
+
22
+ expect(privilegeRejection(context, 'twin', 'mutation').message).toBe('twin:mutation 권한이 필요합니다.')
23
+ })
24
+
25
+ it('번역기가 없어도 이유를 말한다 — 빈 문장이나 예외로 끝나지 않는다', () => {
26
+ for (const context of [undefined, null, {}, { t: 'not a function' }]) {
27
+ const error = privilegeRejection(context, 'twin', 'mutation')
28
+
29
+ expect(error.message).toBe('unauthorized! twin:mutation privilege required')
30
+ }
31
+ })
32
+
33
+ it('번역이 없는 언어에서 키가 새어 나가지 않는다', () => {
34
+ /* i18next 기본 거동을 그대로 쓰면 사용자가 `error.x privilege required` 를 읽는다. */
35
+ const error = privilegeRejection({ t: missing }, 'twin', 'mutation')
36
+
37
+ expect(error.message).toBe('unauthorized! twin:mutation privilege required')
38
+ expect(error.message).not.toContain('error.')
39
+ })
40
+
41
+ it('번역기가 던져도 거절은 그대로 전달된다 — 오류의 원인이 번역기로 바뀌지 않게', () => {
42
+ const context = {
43
+ t: () => {
44
+ throw new Error('i18next not initialized')
45
+ }
46
+ }
47
+
48
+ expect(privilegeRejection(context, 'twin', 'mutation').message).toBe(
49
+ 'unauthorized! twin:mutation privilege required'
50
+ )
51
+ })
52
+
53
+ it('소유권만 요구하는 게이트는 권한 이름을 지어내지 않는다', () => {
54
+ /* `@privilege(domainOwnerGranted: true)` 처럼 category·privilege 가 없는 게이트가 실제로 있다. */
55
+ const error = privilegeRejection({ t: missing })
56
+
57
+ expect(error.message).toBe('unauthorized! domain or system ownership required')
58
+ expect(error.extensions.requires).toBe('ownership')
59
+ })
60
+
61
+ it('무엇이 필요했는지 구조로 싣는다 — 화면이 문장을 다시 파싱하지 않게', () => {
62
+ const error = privilegeRejection({ t: () => '번역된 문장' }, 'twin', 'mutation')
63
+
64
+ expect(error.extensions.code).toBe('FORBIDDEN')
65
+ expect(error.extensions.requires).toEqual({ category: 'twin', privilege: 'mutation' })
66
+ })
67
+
68
+ it('한쪽만 주어진 게이트는 소유권 요구로 본다 — `undefined:mutation` 같은 문장을 만들지 않는다', () => {
69
+ expect(privilegeRejection({}, 'twin', undefined).extensions.requires).toBe('ownership')
70
+ expect(privilegeRejection({}, undefined, 'mutation').extensions.requires).toBe('ownership')
71
+ })
72
+ })
@@ -7,6 +7,7 @@
7
7
  "error.email already exists": "email already used by another user",
8
8
  "error.failed to find x": "failed to find {x}",
9
9
  "error.invalid username": "invalid username: {username}",
10
+ "error.ownership required": "unauthorized! domain or system ownership required",
10
11
  "error.password should be supported": "initial password or default password should be supported",
11
12
  "error.password should match the rule": "password should match following rule. ${rule}",
12
13
  "error.password used in the past": "password used in the past",
@@ -23,6 +24,7 @@
23
24
  "error.user validation failed": "user validation failed",
24
25
  "error.username already exists": "username already used by another user",
25
26
  "error.x is not a member of y": "{x} is not a member of {y}",
27
+ "error.x privilege required": "unauthorized! {x} privilege required",
26
28
  "field.active": "active",
27
29
  "field.appliance_id": "appliance id",
28
30
  "field.brand": "brand",
@@ -7,6 +7,7 @@
7
7
  "error.email already exists": "メールはすでに他のユーザーによって使用されています.",
8
8
  "error.failed to find x": "{x}が見つかりません.",
9
9
  "error.invalid username": "無効なユーザー名: {username}",
10
+ "error.ownership required": "権限がありません。ドメインまたはシステムの所有者権限が必要です。",
10
11
  "error.password should be supported": "初期パスワードまたはデフォルトパスワードがサポートされるべきです",
11
12
  "error.password should match the rule": "パスワードは次の規則を守らなければなりません. {rule}",
12
13
  "error.password used in the past": "過去に使用されたパスワードです.",
@@ -23,6 +24,7 @@
23
24
  "error.user validation failed": "ユーザー確認に失敗しました.",
24
25
  "error.username already exists": "ユーザー名はすでに他のユーザーによって使用されています.",
25
26
  "error.x is not a member of y": "{x}は{y}のメンバーではありません.",
27
+ "error.x privilege required": "権限がありません。{x} 権限が必要です。",
26
28
  "field.active": "アクティブ",
27
29
  "field.appliance_id": "器具ID",
28
30
  "field.brand": "ブランド",
@@ -7,6 +7,7 @@
7
7
  "error.email already exists": "이메일이 이미 사용되고 있습니다.",
8
8
  "error.failed to find x": "{x}을(를) 찾을 수 없습니다.",
9
9
  "error.invalid username": "유효하지 않은 사용자 이름: {username}",
10
+ "error.ownership required": "권한이 없습니다. 도메인 또는 시스템 소유자 권한이 필요합니다.",
10
11
  "error.password should be supported": "초기 비밀번호나 디폴트 비밀번호가 제공되어야 합니다.",
11
12
  "error.password should match the rule": "비밀번호는 다음 규칙을 지켜야 합니다. {rule}",
12
13
  "error.password used in the past": "과거에 사용된 비밀번호입니다.",
@@ -23,6 +24,7 @@
23
24
  "error.user validation failed": "사용자 확인에 실패하였습니다.",
24
25
  "error.username already exists": "사용자 아이디가 이미 사용되고 있습니다.",
25
26
  "error.x is not a member of y": "{x}은(는) {y}의 멤버가 아닙니다.",
27
+ "error.x privilege required": "권한이 없습니다. {x} 권한이 필요합니다.",
26
28
  "field.active": "활성화",
27
29
  "field.appliance_id": "기구 아이디",
28
30
  "field.brand": "브랜드",
@@ -7,6 +7,7 @@
7
7
  "error.email already exists": "Emel telah digunakan oleh pengguna lain",
8
8
  "error.failed to find x": "Gagal mencari {x}",
9
9
  "error.invalid username": "Nama pengguna tidak sah: {username}",
10
+ "error.ownership required": "tidak dibenarkan! kebenaran pemilik domain atau sistem diperlukan",
10
11
  "error.password should be supported": "kata laluan awal atau kata laluan lalai harus disokong",
11
12
  "error.password should match the rule": "Kata laluan harus mematuhi peraturan berikut. ${rule}",
12
13
  "error.password used in the past": "Kata laluan telah digunakan dalam masa lampau",
@@ -23,6 +24,7 @@
23
24
  "error.user validation failed": "Validasi pengguna gagal",
24
25
  "error.username already exists": "Nama pengguna telah digunakan oleh pengguna lain",
25
26
  "error.x is not a member of y": "{x} bukan ahli {y}",
27
+ "error.x privilege required": "tidak dibenarkan! kebenaran {x} diperlukan",
26
28
  "field.active": "Aktif",
27
29
  "field.appliance_id": "Perkakas",
28
30
  "field.brand": "Jenama",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "error.auth error": "认证错误。{message}",
3
3
  "error.authn verification failed": "用户认证失败。",
4
+ "error.ownership required": "没有权限。需要域或系统所有者权限。",
4
5
  "error.user verification failed": "用户验证失败",
5
6
  "error.confirm password not matched": "新密码与确认密码不匹配!",
6
7
  "error.domain mismatch": "证书不适用于该域!",
@@ -24,6 +25,7 @@
24
25
  "error.user validation failed": "用户验证失败!",
25
26
  "error.username already exists": "用户名已被其他用户使用",
26
27
  "error.x is not a member of y": "{x}不是{y}的成员",
28
+ "error.x privilege required": "没有权限。需要 {x} 权限。",
27
29
  "field.active": "激活",
28
30
  "field.appliance_id": "终端机ID",
29
31
  "field.brand": "品牌",