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:
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) {