commit 8a0c4e70212c95d6eeadbff2a4a2df280cbf5ab5
parent b3a88ab5e5a0f26a7a6863ddd256a3e0bb51a460
Author: Yongbin Kim <iam@yongbin.kim>
Date: Tue, 24 Jan 2023 04:25:31 +0900
feat: ACL 기능 추가
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
2 files changed, 182 insertions(+), 0 deletions(-)
diff --git a/lib/security/acl.test.ts b/lib/security/acl.test.ts
@@ -0,0 +1,121 @@
+import { ACL, ACLItem, resolveACL } from '@/lib/security/acl'
+
+describe('ACL', () => {
+ const tests: Array<{
+ name: string
+ token: { uid: number; aclGroup?: string[] } | null
+ acl: ACLItem[]
+ expected: boolean
+ }> = [{
+ name: 'should allow all',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:all', allow: true },
+ ],
+ expected: true,
+ }, {
+ name: 'should deny all',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:all', allow: false },
+ ],
+ expected: false,
+ }, {
+ name: 'should allow member',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:member', allow: true },
+ ],
+ expected: true,
+ }, {
+ name: 'should deny member',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:member', allow: false },
+ ],
+ expected: false,
+ }, {
+ name: 'should allow anon',
+ token: null,
+ acl: [
+ { cond: 'special:anon', allow: true },
+ ],
+ expected: true,
+ }, {
+ name: 'should deny anon',
+ token: null,
+ acl: [
+ { cond: 'special:anon', allow: false },
+ ],
+ expected: false,
+ }, {
+ name: 'should allow user',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'user:1', allow: true },
+ ],
+ expected: true,
+ }, {
+ name: 'should deny user',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'user:1', allow: false },
+ ],
+ expected: false,
+ }, {
+ name: 'should allow group',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'group:test', allow: true },
+ ],
+ expected: true,
+ }, {
+ name: 'should deny group',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'group:test', allow: false },
+ ],
+ expected: false,
+ }, {
+ name: 'should allow whitelisted user',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:all', allow: false },
+ { cond: 'user:1', allow: true },
+ ],
+ expected: true,
+ }, {
+ name: 'should deny blacklisted user',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:all', allow: true },
+ { cond: 'user:1', allow: false },
+ ],
+ expected: false,
+ }, {
+ name: 'should allow whitelisted group',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:all', allow: false },
+ { cond: 'group:test', allow: true },
+ ],
+ expected: true,
+ }, {
+ name: 'should deny blacklisted group',
+ token: { uid: 1, aclGroup: ['test'] },
+ acl: [
+ { cond: 'special:all', allow: true },
+ { cond: 'group:test', allow: false },
+ ],
+ expected: false,
+ }]
+
+ for (const test of tests) {
+ it(test.name, () => {
+ expect(resolveACL(test.token, { test: test.acl }, 'test'))
+ .toBe(test.expected)
+ })
+ }
+})
+
+export {}
diff --git a/lib/security/acl.ts b/lib/security/acl.ts
@@ -0,0 +1,61 @@
+import { AccessTokenPayload } from '@/lib/security/token'
+
+export interface ACLItem {
+ cond: string
+ allow: boolean
+}
+
+export interface ACL {
+ [action: string]: ACLItem[]
+}
+
+export const ACL_ACTION_READ = 'read'
+export const ACL_ACTION_WRITE = 'write'
+export const ACL_ACTION_DELETE = 'delete'
+export const ACL_ACTION_MOVE = 'move'
+export const ACL_ACTION_MANAGE = 'manage'
+
+export function resolveACL (token: AccessTokenPayload | null, acl: ACL | null | undefined, action: string): boolean {
+ const items = acl?.[action]
+ if (items == null || items.length === 0) {
+ return true
+ }
+
+ let allow = false
+ for (const item of items) {
+ allow = resolveACLItem(token, item) ?? allow
+ }
+
+ return allow
+}
+
+function resolveACLItem (token: AccessTokenPayload | null, item: ACLItem): boolean | null {
+ const [prefix, suffix] = item.cond.split(':')
+
+ switch (prefix) {
+ case 'special':
+ return resolveSpecialACLItem(token, suffix, item)
+
+ case 'user':
+ return token?.uid === parseInt(suffix) ? item.allow : null
+
+ case 'group':
+ return token?.aclGroup?.includes(suffix) ? item.allow : null
+
+ default:
+ return null
+ }
+}
+
+function resolveSpecialACLItem (token: AccessTokenPayload | null, suffix: string, item: ACLItem): boolean | null {
+ switch (suffix) {
+ case 'all':
+ return item.allow
+ case 'member':
+ return token != null ? item.allow : null
+ case 'anon':
+ return token == null ? item.allow : null
+ default:
+ return null
+ }
+}