dh_demo

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

commit 1c260dc9436a80dae5a6603e6b8036aadc7df284
parent b04e0b0d1399719726a9fe75de8ac9d42f8421b4
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Mon, 30 Jan 2023 11:46:09 +0900

feat(acl): ACL 저장할 때 검증하도록 수정

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
Mlib/apierror.ts | 1+
Mlib/security/acl.test.ts | 32+++++++++++++++++++++++++++++++-
Mlib/security/acl.ts | 35++++++++++++++++++++++++++++++++++-
Mpages/api/wiki/[slug]/[...path].tsx | 24+++++++++++++++++++++---
Mpages/api/wiki/[slug]/index.ts | 17+++++++++++++++--
5 files changed, 102 insertions(+), 7 deletions(-)

diff --git a/lib/apierror.ts b/lib/apierror.ts @@ -27,6 +27,7 @@ export const ERR_BAD_ID = newErr(ERR_CODE_BAD_ID, 'Bad ID'), ERR_FORBIDDEN = newErr(ERR_CODE_FORBIDDEN, 'Forbidden'), ERR_INTERNAL = newErr(ERR_CODE_INTERNAL, 'Internal Error'), + ERR_INVALID_ACL = newErr(ERR_CODE_INVALID_REQUEST, 'Invalid ACL'), ERR_INVALID_REQUEST = newErr(ERR_CODE_INVALID_REQUEST, 'Invalid request'), ERR_METHOD_NOT_ALLOWED = newErr(ERR_CODE_METHOD_NOT_ALLOWED, 'Method Not Allowed'), ERR_NOT_FOUND = newErr(ERR_CODE_NOT_FOUND, 'Not Found'), diff --git a/lib/security/acl.test.ts b/lib/security/acl.test.ts @@ -1,4 +1,4 @@ -import { ACL, ACLItem, resolveACL } from '@/lib/security/acl' +import { ACL, ACLItem, resolveACL, validateACL } from '@/lib/security/acl' describe('ACL', () => { const tests: Array<{ @@ -118,4 +118,34 @@ describe('ACL', () => { } }) +describe('Validate ACL', () => { + const tests: Array<{ + name: string + acl: object + expected: boolean + }> = [{ + name: 'should be valid', + acl: { + test: [ + { cond: 'special:all', allow: true }, + { cond: 'special:member', allow: true }, + ], + }, + expected: true, + }, { + name: 'should be invalid', + acl: { + test: { cond: 'special:all', allow: true }, + }, + expected: false, + }] + + for (const test of tests) { + it(test.name, () => { + expect(validateACL(test.acl)) + .toBe(test.expected) + }) + } +}) + export {} diff --git a/lib/security/acl.ts b/lib/security/acl.ts @@ -1,5 +1,4 @@ import { AccessTokenPayload } from '@/lib/security/token' -import { Connection } from 'mysql2/promise' export interface ACLItem { cond: string @@ -95,3 +94,37 @@ function resolveSpecialACLItem (token: AccessTokenPayload | null, suffix: string return null } } + +export function validateACL (acl: any): acl is ACL { + if (typeof acl !== 'object' || acl == null) { + return false + } + + for (const key in acl) { + if (!validateACLItems(acl[key])) { + return false + } + } + + return true +} + +function validateACLItems (aclItems: any): aclItems is ACLItem[] { + if (!Array.isArray(aclItems)) { + return false + } + + for (const aclItem of aclItems) { + if (!validateACLItem(aclItem)) { + return false + } + } + + return true +} + +function validateACLItem (aclItem: any): aclItem is ACLItem { + return typeof aclItem === 'object' && aclItem != null && + typeof aclItem.cond === 'string' && + typeof aclItem.allow === 'boolean' +} diff --git a/pages/api/wiki/[slug]/[...path].tsx b/pages/api/wiki/[slug]/[...path].tsx @@ -1,4 +1,4 @@ -import { ApiError, ERR_NOT_FOUND } from '@/lib/apierror' +import { ApiError, ERR_FORBIDDEN, ERR_INVALID_ACL, ERR_NOT_FOUND } from '@/lib/apierror' import { ERR_CODE_EMPTY_CONTENT } from '@/lib/error_codes' import { withConnection } from '@/lib/model_helpers' import { getWikiAndPageACLViaPath } from '@/lib/models/wiki_acl' @@ -6,7 +6,8 @@ import { createWikiChange } from '@/lib/models/wiki_change' import { getWiki, getWikiViaSlug } from '@/lib/models/wiki_info' import { getWikiPage as modelGetWikiPage, putWikiPage, updateWikiPageAcl } from '@/lib/models/wiki_page' import { createWikiText } from '@/lib/models/wiki_text' -import { ACL, ACL_ACTION_MANAGE, ACL_ACTION_READ, ACL_ACTION_WRITE, resolveACL } from '@/lib/security/acl' +import { getRedis } from '@/lib/redis' +import { ACL_ACTION_MANAGE, ACL_ACTION_READ, ACL_ACTION_WRITE, resolveACL, validateACL } from '@/lib/security/acl' import { AccessTokenPayload, authenticationFromCookies } from '@/lib/security/token' import { getRemoteIp } from '@/lib/utils/ip' import { getSlugAndPath } from '@/lib/utils/wiki' @@ -109,9 +110,23 @@ async function handlePut ( if (acl != null) { if (aclInfo.pageId == null || !resolveACL(token, aclInfo.wiki, ACL_ACTION_MANAGE)) { - res.status(403).json({ code: 'ERR_FORBIDDEN', message: 'forbidden' }) + res.status(403).json(ERR_FORBIDDEN) return } + + let aclData; + try { + aclData = JSON.parse(acl) + } catch (e) { + res.status(400).json(ERR_INVALID_ACL) + return + } + + if (!validateACL(aclData)) { + res.status(400).json(ERR_INVALID_ACL) + return + } + await updateWikiPageAcl(conn, [aclInfo.pageId, acl]) } @@ -129,6 +144,9 @@ async function handlePut ( token?.uid == null ? getRemoteIp(req) : null, textId, ]) + + const redis = await getRedis() + await redis.publish('event', `/edit/${slug}/${path}|pageChange`) } res.status(200).json({ status: 'ok' }) diff --git a/pages/api/wiki/[slug]/index.ts b/pages/api/wiki/[slug]/index.ts @@ -1,7 +1,7 @@ -import { ApiError, ERR_INTERNAL, ERR_METHOD_NOT_ALLOWED, ERR_NOT_FOUND } from '@/lib/apierror' +import { ApiError, ERR_INTERNAL, ERR_INVALID_ACL, ERR_METHOD_NOT_ALLOWED, ERR_NOT_FOUND } from '@/lib/apierror' import { withConnection } from '@/lib/model_helpers' import { getWiki, getWikiViaSlug, updateWiki, WikiInfo } from '@/lib/models/wiki_info' -import { ACL_ACTION_MANAGE, ACL_ACTION_READ, resolveACL } from '@/lib/security/acl' +import { ACL_ACTION_MANAGE, ACL_ACTION_READ, resolveACL, validateACL } from '@/lib/security/acl' import { authenticationFromCookies } from '@/lib/security/token' import { getSlugAndPath } from '@/lib/utils/wiki' import { NextApiRequest, NextApiResponse } from 'next' @@ -82,6 +82,19 @@ export async function handlePatch ( const {title, description, acl} = req.body as PatchWikiBody + let aclData; + try { + aclData = JSON.parse(acl) + } catch (e) { + res.status(400).json(ERR_INVALID_ACL) + return + } + + if (!validateACL(aclData)) { + res.status(400).json(ERR_INVALID_ACL) + return + } + try { await updateWiki(conn, [title, description, acl, wiki.id]) } catch (e) {