isAdmin implemented.
This commit is contained in:
38
index.js
38
index.js
@ -18,6 +18,9 @@ export const securityEnhancer = (log, loga, argv, baseHandler) => {
|
|||||||
// Initialize token manager
|
// Initialize token manager
|
||||||
const tokenManager = new TokenManager(argv.status)
|
const tokenManager = new TokenManager(argv.status)
|
||||||
|
|
||||||
|
// Get admin configuration from argv (same way other security plugins do)
|
||||||
|
const admin = argv.admin
|
||||||
|
|
||||||
// Helper function to get current user from enhanced authentication
|
// Helper function to get current user from enhanced authentication
|
||||||
const getCurrentUser = (req) => {
|
const getCurrentUser = (req) => {
|
||||||
return enhancer.getUser(req, () => baseHandler.getUser(req))
|
return enhancer.getUser(req, () => baseHandler.getUser(req))
|
||||||
@ -38,6 +41,23 @@ export const securityEnhancer = (log, loga, argv, baseHandler) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced admin check that includes token-based admin
|
||||||
|
enhancer.isAdmin = (req, baseIsAdmin) => {
|
||||||
|
// First check if base authentication already grants admin
|
||||||
|
if (baseIsAdmin()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token belongs to admin user
|
||||||
|
if (req.tokenAuth && req.tokenAuth.user && admin !== undefined) {
|
||||||
|
// Compare token user with configured admin
|
||||||
|
// Use JSON.stringify for deep comparison like we do elsewhere
|
||||||
|
return JSON.stringify(req.tokenAuth.user) === JSON.stringify(admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced user identification that includes token users
|
// Enhanced user identification that includes token users
|
||||||
enhancer.getUser = (req, baseGetUser) => {
|
enhancer.getUser = (req, baseGetUser) => {
|
||||||
// If we have token auth, return the token user
|
// If we have token auth, return the token user
|
||||||
@ -118,13 +138,27 @@ export const securityEnhancer = (log, loga, argv, baseHandler) => {
|
|||||||
app.post('/plugin/useraccesstokens/create', authenticated, async (req, res) => {
|
app.post('/plugin/useraccesstokens/create', authenticated, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = getCurrentUser(req)
|
const user = getCurrentUser(req)
|
||||||
const { name, expiresInDays } = req.body
|
const { name, expiresInDays, scopes } = req.body
|
||||||
|
|
||||||
if (!name || typeof name !== 'string' || name.trim() === '') {
|
if (!name || typeof name !== 'string' || name.trim() === '') {
|
||||||
return res.status(400).json({ error: 'Token name is required' })
|
return res.status(400).json({ error: 'Token name is required' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await tokenManager.createToken(user, name.trim(), expiresInDays)
|
// Validate scopes if provided
|
||||||
|
if (scopes && !Array.isArray(scopes)) {
|
||||||
|
return res.status(400).json({ error: 'Scopes must be an array' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter valid scopes - admin scope is not needed since admin is based on user
|
||||||
|
const validScopes = ['site:read', 'site:write']
|
||||||
|
const requestedScopes = scopes || ['site:read', 'site:write']
|
||||||
|
const filteredScopes = requestedScopes.filter(scope => validScopes.includes(scope))
|
||||||
|
|
||||||
|
if (requestedScopes.length > 0 && filteredScopes.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'No valid scopes provided' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tokenManager.createToken(user, name.trim(), expiresInDays, filteredScopes)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
token: result.token,
|
token: result.token,
|
||||||
|
@ -53,7 +53,7 @@ class TokenManager {
|
|||||||
return token.slice(-4)
|
return token.slice(-4)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createToken(user, name, expiresInDays = null) {
|
async createToken(user, name, expiresInDays = null, scopes = null) {
|
||||||
const tokens = await this.loadTokens()
|
const tokens = await this.loadTokens()
|
||||||
|
|
||||||
// Check if token name already exists for this user
|
// Check if token name already exists for this user
|
||||||
@ -67,6 +67,9 @@ class TokenManager {
|
|||||||
const created = new Date().toISOString()
|
const created = new Date().toISOString()
|
||||||
const expires = expiresInDays ? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString() : null
|
const expires = expiresInDays ? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString() : null
|
||||||
|
|
||||||
|
// Default scopes if none provided
|
||||||
|
const tokenScopes = scopes || ['site:read', 'site:write']
|
||||||
|
|
||||||
const tokenRecord = {
|
const tokenRecord = {
|
||||||
name,
|
name,
|
||||||
user,
|
user,
|
||||||
@ -76,7 +79,7 @@ class TokenManager {
|
|||||||
expires,
|
expires,
|
||||||
lastUsed: null,
|
lastUsed: null,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
scopes: ['site:read', 'site:write'] // Default scopes
|
scopes: tokenScopes
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens.push(tokenRecord)
|
tokens.push(tokenRecord)
|
||||||
|
184
test/admin-functionality.test.js
Normal file
184
test/admin-functionality.test.js
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { suite, test } from 'node:test'
|
||||||
|
import assert from 'node:assert'
|
||||||
|
import { TokenManager } from '../server/server.js'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
// Import the securityEnhancer to test isAdmin
|
||||||
|
import { securityEnhancer } from '../index.js'
|
||||||
|
|
||||||
|
suite('isAdmin functionality with tokens', () => {
|
||||||
|
let tempDir
|
||||||
|
let tokenManager
|
||||||
|
let enhancer
|
||||||
|
let adminUser
|
||||||
|
let regularUser
|
||||||
|
let adminToken
|
||||||
|
let regularToken
|
||||||
|
|
||||||
|
const setup = async () => {
|
||||||
|
tempDir = path.join(__dirname, 'temp-admin-' + Date.now())
|
||||||
|
await fs.mkdir(tempDir, { recursive: true })
|
||||||
|
|
||||||
|
tokenManager = new TokenManager(tempDir)
|
||||||
|
|
||||||
|
adminUser = {
|
||||||
|
displayName: 'Admin User',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
provider: 'github',
|
||||||
|
id: 'admin123'
|
||||||
|
}
|
||||||
|
|
||||||
|
regularUser = {
|
||||||
|
displayName: 'Regular User',
|
||||||
|
email: 'user@example.com',
|
||||||
|
provider: 'github',
|
||||||
|
id: 'user456'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tokens for both users
|
||||||
|
const adminResult = await tokenManager.createToken(adminUser, 'admin-token')
|
||||||
|
adminToken = adminResult.token
|
||||||
|
|
||||||
|
const regularResult = await tokenManager.createToken(regularUser, 'regular-token')
|
||||||
|
regularToken = regularResult.token
|
||||||
|
|
||||||
|
// Create the enhancer with admin configuration
|
||||||
|
const mockLog = console.log
|
||||||
|
const mockLoga = console.log
|
||||||
|
const mockArgv = {
|
||||||
|
status: tempDir,
|
||||||
|
admin: adminUser // Configure admin user
|
||||||
|
}
|
||||||
|
const mockBaseHandler = {
|
||||||
|
getUser: (req) => req.user || null,
|
||||||
|
isAuthorized: () => false,
|
||||||
|
isAdmin: () => false // Base handler doesn't grant admin access
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancer = securityEnhancer(mockLog, mockLoga, mockArgv, mockBaseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
if (tempDir) {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('isAdmin returns true for tokens belonging to admin users', async () => {
|
||||||
|
await setup()
|
||||||
|
try {
|
||||||
|
const req = {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${adminToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up token auth context (normally done by middleware)
|
||||||
|
await enhancer.middleware(req, {}, () => {})
|
||||||
|
|
||||||
|
// Test isAdmin
|
||||||
|
const baseIsAdmin = () => false
|
||||||
|
const isAdmin = enhancer.isAdmin(req, baseIsAdmin)
|
||||||
|
|
||||||
|
assert.equal(isAdmin, true)
|
||||||
|
} finally {
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAdmin returns false for tokens belonging to regular users', async () => {
|
||||||
|
await setup()
|
||||||
|
try {
|
||||||
|
const req = {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${regularToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up token auth context (normally done by middleware)
|
||||||
|
await enhancer.middleware(req, {}, () => {})
|
||||||
|
|
||||||
|
// Test isAdmin
|
||||||
|
const baseIsAdmin = () => false
|
||||||
|
const isAdmin = enhancer.isAdmin(req, baseIsAdmin)
|
||||||
|
|
||||||
|
assert.equal(isAdmin, false)
|
||||||
|
} finally {
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAdmin respects base admin when base returns true', async () => {
|
||||||
|
await setup()
|
||||||
|
try {
|
||||||
|
const req = {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${regularToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up token auth context (normally done by middleware)
|
||||||
|
await enhancer.middleware(req, {}, () => {})
|
||||||
|
|
||||||
|
// Test isAdmin with base admin returning true (session-based admin)
|
||||||
|
const baseIsAdmin = () => true
|
||||||
|
const isAdmin = enhancer.isAdmin(req, baseIsAdmin)
|
||||||
|
|
||||||
|
assert.equal(isAdmin, true)
|
||||||
|
} finally {
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAdmin returns false when no token auth present', async () => {
|
||||||
|
await setup()
|
||||||
|
try {
|
||||||
|
const req = { headers: {} }
|
||||||
|
|
||||||
|
// Test isAdmin without token auth
|
||||||
|
const baseIsAdmin = () => false
|
||||||
|
const isAdmin = enhancer.isAdmin(req, baseIsAdmin)
|
||||||
|
|
||||||
|
assert.equal(isAdmin, false)
|
||||||
|
} finally {
|
||||||
|
await cleanup()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAdmin returns false when no admin is configured', async () => {
|
||||||
|
// Create enhancer without admin configuration
|
||||||
|
const tempDir2 = path.join(__dirname, 'temp-no-admin-' + Date.now())
|
||||||
|
await fs.mkdir(tempDir2, { recursive: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mockArgv = { status: tempDir2 } // No admin configured
|
||||||
|
const mockBaseHandler = {
|
||||||
|
getUser: () => null,
|
||||||
|
isAuthorized: () => false,
|
||||||
|
isAdmin: () => false
|
||||||
|
}
|
||||||
|
|
||||||
|
const enhancerNoAdmin = securityEnhancer(console.log, console.log, mockArgv, mockBaseHandler)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
tokenAuth: {
|
||||||
|
user: adminUser,
|
||||||
|
scopes: ['site:read', 'site:write'],
|
||||||
|
tokenName: 'test-token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseIsAdmin = () => false
|
||||||
|
const isAdmin = enhancerNoAdmin.isAdmin(req, baseIsAdmin)
|
||||||
|
|
||||||
|
assert.equal(isAdmin, false)
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir2, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
Reference in New Issue
Block a user