Introduce Casbin Middleware (#676)

* feat: impl casbin middleware

* fix: defaultCheckPermission logic

* fix: divide authorizer

* chore: testing and docs

* fixed: hono version

* fix: export and module resolution

* fix: typo

* docs: fix model

* fix: conflict

* fix: version
pull/736/head
sugar 2024-09-09 21:56:28 +09:00 committed by GitHub
parent 69b3cfe726
commit b7e740f930
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1176 additions and 7 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/casbin': major
---
Initial release

25
.github/workflows/ci-casbin.yml vendored 100644
View File

@ -0,0 +1,25 @@
name: ci-cabin
on:
push:
branches: [main]
paths:
- 'packages/cabin/**'
pull_request:
branches: ['*']
paths:
- 'packages/cabin/**'
jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/cabin
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test

View File

@ -35,6 +35,7 @@
"build:react-compat": "yarn workspace @hono/react-compat build", "build:react-compat": "yarn workspace @hono/react-compat build",
"build:effect-validator": "yarn workspace @hono/effect-validator build", "build:effect-validator": "yarn workspace @hono/effect-validator build",
"build:conform-validator": "yarn workspace @hono/conform-validator build", "build:conform-validator": "yarn workspace @hono/conform-validator build",
"build:casbin": "yarn workspace @hono/casbin build",
"build": "run-p 'build:*'", "build": "run-p 'build:*'",
"lint": "eslint 'packages/**/*.{ts,tsx}'", "lint": "eslint 'packages/**/*.{ts,tsx}'",
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'", "lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",

View File

@ -0,0 +1,140 @@
# Casbin Middleware for Hono
This is a third-party [Casbin](https://casbin.org) middleware for [Hono](https://github.com/honojs/hono).
This middleware can be used to enforce authorization policies defined using Casbin in your Hono routes.
## Installation
```bash
npm i hono @hono/casbin casbin
```
## Configuration
Before using the middleware, you must set up your Casbin model and policy files.
For details on how to write authorization policies and other information, please refer to the [Casbin documentation](https://casbin.org/).
### Example model.conf
```conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
```
### Example policy.csv
```csv
p, alice, /dataset1/*, *
p, bob, /dataset1/*, GET
```
## Usage with Basic HTTP Authentication
You can perform authorization control after Basic authentication by combining it with `basicAuthorizer`.
(The client needs to send `Authentication: Basic {Base64Encoded(username:password)}`.)
Let's look at an example.
Use the `model` and `policy` files from the [Configuration](#configuration) section.
You can implement a scenario where `alice` and `bob` have different permissions. Alice has access to all methods on `/dataset1/test`, while Bob has access only to the `GET` method.
```ts
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
import { newEnforcer } from 'casbin'
import { casbin } from '@hono/casbin'
import { basicAuthorizer } from '@hono/casbin/helper'
const app = new Hono()
app.use('*',
basicAuth(
{
username: 'alice', // alice has full access to /dataset1/test
password: 'password',
},
{
username: 'bob', // bob cannot post to /dataset1/test
password: 'password',
}
),
casbin({
newEnforcer: newEnforcer('examples/model.conf', 'examples/policy.csv'),
authorizer: basicAuthorizer
})
)
app.get('/dataset1/test', (c) => c.text('dataset1 test')) // alice and bob can access /dataset1/test
app.post('/dataset1/test', (c) => c.text('dataset1 test')) // Only alice can access /dataset1/test
```
## Usage with JWT Authentication
By using `jwtAuthorizer`, you can perform authorization control after JWT authentication.
By default, `jwtAuthorizer` uses the `sub` in the JWT payload as the username.
```ts
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import { newEnforcer } from 'casbin'
import { casbin } from '@hono/casbin'
import { jwtAuthorizer } from '@hono/casbin/helper'
const app = new Hono()
app.use('*',
jwt({
secret: 'it-is-very-secret',
}),
casbin({
newEnforcer: newEnforcer('examples/model.conf', 'examples/policy.csv'),
authorizer: jwtAuthorizer
})
)
app.get('/dataset1/test', (c) => c.text('dataset1 test')) // alice and bob can access /dataset1/test
app.post('/dataset1/test', (c) => c.text('dataset1 test')) // Only alice can access /dataset1/test
```
Of course, you can use claims other than the `sub` claim.
Specify the `key` as a user-friendly name and the `value` as the JWT claim name. The `Payload` key used for evaluation in the enforcer will be the `value`.
```ts
const claimMapping = {
username: 'username',
}
// ...
casbin({
newEnforcer: newEnforcer('examples/model.conf', 'examples/policy.csv'),
authorizer: (c, e) => jwtAuthorizer(c, e, claimMapping)
})
```
## Usage with Customized Authorizer
You can also use a customized authorizer function to handle the authorization logic.
```ts
import { Hono } from 'hono'
import { newEnforcer } from 'casbin'
import { casbin } from '@hono/casbin'
const app = new Hono()
app.use('*', casbin({
newEnforcer: newEnforcer('path-to-your-model.conf', 'path-to-your-policy.csv'),
authorizer: async (c, enforcer) => {
const { user, path, method } = c
return await enforcer.enforce(user, path, method)
}
}))
```
## Author
sugar-cat https://github.com/sugar-cat7

View File

@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")

View File

@ -0,0 +1,5 @@
p, dataset1_admin, /dataset1/*, *
p, dataset2_admin, /dataset2/*, *
g, alice, dataset1_admin
g, bob, dataset2_admin
1 p, dataset1_admin, /dataset1/*, *
2 p, dataset2_admin, /dataset2/*, *
3 g, alice, dataset1_admin
4 g, bob, dataset2_admin

View File

@ -0,0 +1,61 @@
{
"name": "@hono/casbin",
"version": "0.0.0",
"description": "Casbin middleware for Hono",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./helper": {
"import": {
"types": "./dist/helper/index.d.ts",
"default": "./dist/helper/index.js"
},
"require": {
"types": "./dist/helper/index.d.cts",
"default": "./dist/helper/index.cjs"
}
}
},
"files": [
"dist"
],
"scripts": {
"test": "vitest --run",
"build": "tsup ./src/index.ts ./src/helper/index.ts --format esm,cjs --dts",
"publint": "publint",
"release": "yarn build && yarn test && yarn publint && yarn publish"
},
"license": "MIT",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/honojs/middleware.git"
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"casbin": ">=5.30.0",
"hono": ">=4.5.11"
},
"devDependencies": {
"casbin": "^5.30.0",
"hono": "^4.5.11",
"tsup": "^8.1.0",
"typescript": "^5.5.3",
"vitest": "^2.0.1"
}
}

View File

@ -0,0 +1,17 @@
import type { Enforcer } from 'casbin'
import type { Context } from 'hono'
import { auth } from 'hono/utils/basic-auth'
const getUserName = (c: Context): string => {
const requestUser = auth(c.req.raw)
if (!requestUser) {
return ''
}
return requestUser.username
}
export const basicAuthorizer = async (c: Context, enforcer: Enforcer): Promise<boolean> => {
const { path, method } = c.req
const user = getUserName(c)
return enforcer.enforce(user, path, method)
}

View File

@ -0,0 +1,2 @@
export * from './jwt'
export * from './basic-auth'

View File

@ -0,0 +1,36 @@
import { decode } from 'hono/jwt'
import type { Enforcer } from 'casbin'
import type { Context } from 'hono'
import type { JWTPayload } from 'hono/utils/jwt/types'
export const jwtAuthorizer = async (
c: Context,
enforcer: Enforcer,
claimMapping: Record<string, string> = { userID: 'sub' }
): Promise<boolean> => {
// Note: if use hono/jwt, the payload is stored in c.get('jwtPayload')
// https://github.com/honojs/hono/blob/8ba02273e829318d7f8797267f52229e531b8bd5/src/middleware/jwt/jwt.ts#L136
let payload: JWTPayload = c.get('jwtPayload')
if (!payload) {
const credentials = c.req.header('Authorization')
if (!credentials) return false
const parts = credentials.split(/\s+/)
if (parts.length !== 2 || parts[0] !== 'Bearer') return false
const token = parts[1]
try {
const decoded = decode(token)
payload = decoded.payload
} catch {
return false
}
}
const args = Object.values(claimMapping).map((key) => payload[key])
const { path, method } = c.req
return await enforcer.enforce(...args, path, method)
}

View File

@ -0,0 +1,22 @@
import { Enforcer } from 'casbin'
import { type Context, MiddlewareHandler } from 'hono'
interface CasbinOptions {
newEnforcer: Promise<Enforcer>
authorizer: (c: Context, enforcer: Enforcer) => Promise<boolean>
}
export const casbin = (opt: CasbinOptions): MiddlewareHandler => {
return async (c, next) => {
const enforcer = await opt.newEnforcer
if (!(enforcer instanceof Enforcer)) {
return c.json({ error: 'Invalid enforcer' }, 500)
}
const isAllowed = await opt.authorizer(c, enforcer)
if (!isAllowed) {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
}
}

View File

@ -0,0 +1,738 @@
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { newEnforcer } from 'casbin'
import { casbin } from '../src'
import { basicAuthorizer, jwtAuthorizer } from '../src/helper'
import { jwt, sign } from 'hono/jwt'
import { basicAuth } from 'hono/basic-auth'
describe('Casbin Middleware Tests', () => {
describe('BasicAuthorizer', () => {
const app = new Hono()
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
app.use('*', casbin({ newEnforcer: enforcer, authorizer: basicAuthorizer }))
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
it('test[Success]: p, alice, /dataset1/*, GET', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, POST', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, PUT', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, DELETE', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Not Exist User]: p, cathy, /dataset1/*, - GET 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p, cathy, /dataset1/*, - POST 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p, cathy, /dataset1/*, - PUT 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p, cathy, /dataset1/*, - DELETE 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Basic ${Buffer.from('cathy:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - GET 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - POST 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - PUT 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - DELETE 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
})
describe('BasicAuthorizer with hono/basic-auth', () => {
const app = new Hono()
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
app.use(
'*',
basicAuth(
{
username: 'alice',
password: 'password',
},
{
username: 'bob',
password: 'password',
}
),
casbin({ newEnforcer: enforcer, authorizer: basicAuthorizer })
)
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
it('test[Success]: p, alice, /dataset1/*, GET', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, POST', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, PUT', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, DELETE', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Basic ${Buffer.from('alice:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - GET 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - POST 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - PUT 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/*, - DELETE 403', async () => {
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Basic ${Buffer.from('bob:password').toString('base64')}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
})
describe('JWTAuthorizer', () => {
const app = new Hono()
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
app.use('*', casbin({ newEnforcer: enforcer, authorizer: jwtAuthorizer }))
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
it('test[Success]: p, alice, /dataset1/*, GET 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, POST 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, PUT 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, DELETE 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - GET 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - POST 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - PUT 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - DELETE 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - GET 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - POST 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - PUT 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - DELETE 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
})
describe('JWTAuthorizer With Custom Claim', () => {
const app = new Hono()
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
const customClaimMapping = { userID: 'custom_id' }
app.use(
'*',
casbin({
newEnforcer: enforcer,
authorizer: (c, e) => jwtAuthorizer(c, e, customClaimMapping),
})
)
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
it('test[Success]: p, alice, /dataset1/*, GET 200', async () => {
const token = await sign({ custom_id: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, POST 200', async () => {
const token = await sign({ custom_id: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, PUT 200', async () => {
const token = await sign({ custom_id: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, DELETE 200', async () => {
const token = await sign({ custom_id: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - GET 403', async () => {
const token = await sign({ custom_id: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - POST 403', async () => {
const token = await sign({ custom_id: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - PUT 403', async () => {
const token = await sign({ custom_id: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - DELETE 403', async () => {
const token = await sign({ custom_id: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - GET 403', async () => {
const token = await sign({ custom_id: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - POST 403', async () => {
const token = await sign({ custom_id: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - PUT 403', async () => {
const token = await sign({ custom_id: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - DELETE 403', async () => {
const token = await sign({ custom_id: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
})
describe('JWTAuthorizer With hono/jwt', () => {
const app = new Hono()
const enforcer = newEnforcer('examples/model.conf', 'examples/policy.csv')
app.use(
'*',
jwt({
secret: 'secret',
}),
casbin({ newEnforcer: enforcer, authorizer: jwtAuthorizer })
)
app.get('/dataset1/test', (c) => c.text('dataset1 test'))
app.post('/dataset1/test', (c) => c.text('dataset1 test'))
app.put('/dataset1/test', (c) => c.text('dataset1 test'))
app.delete('/dataset1/test', (c) => c.text('dataset1 test'))
it('test[Success]: p, alice, /dataset1/*, GET 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, POST 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, PUT 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Success]: p, alice, /dataset1/*, DELETE 200', async () => {
const token = await sign({ sub: 'alice' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(200)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - GET 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - POST 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - PUT 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Not Exist User]: p & g, dataset1_admin, /dataset1/*, * - DELETE 403', async () => {
const token = await sign({ sub: 'cathy' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - GET 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - POST 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - PUT 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
it('test[Insufficient Permissions]: p, bob, /dataset1/test - DELETE 403', async () => {
const token = await sign({ sub: 'bob' }, 'secret')
const req = new Request('http://localhost/dataset1/test', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
})
const res = await app.fetch(req)
expect(res.status).toBe(403)
})
})
})

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"exactOptionalPropertyTypes": true
},
"include": [
"src/**/*.ts"
],
}

View File

@ -0,0 +1,8 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
},
})

View File

@ -2278,6 +2278,21 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@hono/casbin@workspace:packages/casbin":
version: 0.0.0-use.local
resolution: "@hono/casbin@workspace:packages/casbin"
dependencies:
casbin: "npm:^5.30.0"
hono: "npm:^4.5.11"
tsup: "npm:^8.1.0"
typescript: "npm:^5.5.3"
vitest: "npm:^2.0.1"
peerDependencies:
casbin: ">=5.30.0"
hono: ">=4.5.11"
languageName: unknown
linkType: soft
"@hono/clerk-auth@workspace:packages/clerk-auth": "@hono/clerk-auth@workspace:packages/clerk-auth":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@hono/clerk-auth@workspace:packages/clerk-auth" resolution: "@hono/clerk-auth@workspace:packages/clerk-auth"
@ -6303,6 +6318,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"await-lock@npm:^2.0.1":
version: 2.2.2
resolution: "await-lock@npm:2.2.2"
checksum: bedf00dad44c6325a655bf3bd523ab9e1ce41023da6a8c379990c76ac1d942ac7e5301627ab84ba37917ab5247506ba429b7f6e4bf77074093f255571b9ad5ee
languageName: node
linkType: hard
"babel-jest@npm:^28.1.3": "babel-jest@npm:^28.1.3":
version: 28.1.3 version: 28.1.3
resolution: "babel-jest@npm:28.1.3" resolution: "babel-jest@npm:28.1.3"
@ -6689,6 +6711,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"buffer@npm:^6.0.3":
version: 6.0.3
resolution: "buffer@npm:6.0.3"
dependencies:
base64-js: "npm:^1.3.1"
ieee754: "npm:^1.2.1"
checksum: 2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0
languageName: node
linkType: hard
"builtins@npm:^1.0.3": "builtins@npm:^1.0.3":
version: 1.0.3 version: 1.0.3
resolution: "builtins@npm:1.0.3" resolution: "builtins@npm:1.0.3"
@ -6903,6 +6935,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"casbin@npm:^5.30.0":
version: 5.30.0
resolution: "casbin@npm:5.30.0"
dependencies:
await-lock: "npm:^2.0.1"
buffer: "npm:^6.0.3"
csv-parse: "npm:^5.3.5"
expression-eval: "npm:^5.0.0"
minimatch: "npm:^7.4.2"
checksum: e3fee7b9b94f4cb6ae9b550fda80f64c46998828fb1c57b3516f6d66e1c98c6e928fa21e5c48af9d831d2dc066a24199d324d4f0866990d244ce3652ae8af509
languageName: node
linkType: hard
"catharsis@npm:^0.9.0": "catharsis@npm:^0.9.0":
version: 0.9.0 version: 0.9.0
resolution: "catharsis@npm:0.9.0" resolution: "catharsis@npm:0.9.0"
@ -7745,6 +7790,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"csv-parse@npm:^5.3.5":
version: 5.5.6
resolution: "csv-parse@npm:5.5.6"
checksum: b4f6e9b885e4488829356455157bd009f3fed4119c5fbaadab1a879e85f0a9a1b62cd01e6de68ff77a50f820a6261722bce1b799da1ace2e2126e0b7c8d86760
languageName: node
linkType: hard
"csv-stringify@npm:^5.6.5": "csv-stringify@npm:^5.6.5":
version: 5.6.5 version: 5.6.5
resolution: "csv-stringify@npm:5.6.5" resolution: "csv-stringify@npm:5.6.5"
@ -9735,6 +9787,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"expression-eval@npm:^5.0.0":
version: 5.0.1
resolution: "expression-eval@npm:5.0.1"
dependencies:
jsep: "npm:^0.3.0"
checksum: 74f9e1e54e50b3c924a71bcddf1550c51f15e24646f2b6cb8c45c7dd3731eb3f0e1e9a6dbf895549ddc445fe66909c373779ea6bf7f6f1e90d6bcef1590543ff
languageName: node
linkType: hard
"extend@npm:^3.0.0, extend@npm:^3.0.2": "extend@npm:^3.0.0, extend@npm:^3.0.2":
version: 3.0.2 version: 3.0.2
resolution: "extend@npm:3.0.2" resolution: "extend@npm:3.0.2"
@ -11050,9 +11111,16 @@ __metadata:
linkType: hard linkType: hard
"hono@npm:^4.5.1": "hono@npm:^4.5.1":
version: 4.5.1 version: 4.5.3
resolution: "hono@npm:4.5.1" resolution: "hono@npm:4.5.3"
checksum: 71228cefd3808b4bb42c10de23d67f135678eb0ab7fdfd1728d934ec2ee1241be9b1260f279b81acd2a6a57346d06d047a2650ff9bddd48fda6581751a690d80 checksum: 360fec2ea66b85d688dd9ce50eb6cdf94f6a2b8508e0b37b688fda3a94e7438cc4c143c0282b7aea41f7be8fa69f0b6d0039931fc8e1526c11ae09303dccce30
languageName: node
linkType: hard
"hono@npm:^4.5.11":
version: 4.5.11
resolution: "hono@npm:4.5.11"
checksum: 839c90273b17ed3797d34a19d12dc577d6f98590a4261bf87488880db13137c0c84a165e7810dc50af2ce4680c8fc915c0bb65673bf5fc54c6d951f18454b2c5
languageName: node languageName: node
linkType: hard linkType: hard
@ -11218,7 +11286,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ieee754@npm:^1.1.13": "ieee754@npm:^1.1.13, ieee754@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "ieee754@npm:1.2.1" resolution: "ieee754@npm:1.2.1"
checksum: b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb checksum: b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
@ -13118,6 +13186,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jsep@npm:^0.3.0":
version: 0.3.5
resolution: "jsep@npm:0.3.5"
checksum: fb5def7a4ba1cee41d144ebdd0d477785dc84b6bc1fed6cf5169f106de980dbe363bf99cb36a450435d7fd952d22b1d76e1609aeb5c7e7cbbbdb6d15fad03614
languageName: node
linkType: hard
"jsesc@npm:^2.5.1": "jsesc@npm:^2.5.1":
version: 2.5.2 version: 2.5.2
resolution: "jsesc@npm:2.5.2" resolution: "jsesc@npm:2.5.2"
@ -14843,9 +14918,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minimatch@npm:^9.0.0, minimatch@npm:^9.0.3, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": "minimatch@npm:^7.4.2":
version: 9.0.5 version: 7.4.6
resolution: "minimatch@npm:9.0.5" resolution: "minimatch@npm:7.4.6"
dependencies:
brace-expansion: "npm:^2.0.1"
checksum: e587bf3d90542555a3d58aca94c549b72d58b0a66545dd00eef808d0d66e5d9a163d3084da7f874e83ca8cc47e91c670e6c6f6593a3e7bb27fcc0e6512e87c67
languageName: node
linkType: hard
"minimatch@npm:^9.0.1":
version: 9.0.3
resolution: "minimatch@npm:9.0.3"
dependencies: dependencies:
brace-expansion: "npm:^2.0.1" brace-expansion: "npm:^2.0.1"
checksum: de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed checksum: de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed