dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

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:
Alib/security/acl.test.ts | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/security/acl.ts | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 + } +}