feat(security): implement secure database operations with access control and logging

This commit is contained in:
2025-12-25 18:08:27 +00:00
parent 8d5efd2b17
commit 326d18d36d
9 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import type { SecurityContext, OperationType, ResourceType } from './types'
import { checkRateLimit } from './check-rate-limit'
import { checkAccess } from './check-access'
import { logOperation } from './log-operation'
/**
* Execute a secure database query with rate limiting, access control, and audit logging
*/
export async function executeQuery<T>(
ctx: SecurityContext,
resource: ResourceType,
operation: OperationType,
queryFn: () => Promise<T>,
resourceId: string = 'unknown'
): Promise<T> {
// Check rate limit
const canProceed = checkRateLimit(ctx.user.id)
if (!canProceed) {
await logOperation(ctx, operation, resource, resourceId, false, 'Rate limit exceeded')
throw new Error('Rate limit exceeded. Please try again later.')
}
// Check access permissions
const hasAccess = await checkAccess(ctx, resource, operation, resourceId)
if (!hasAccess) {
await logOperation(ctx, operation, resource, resourceId, false, 'Access denied')
throw new Error('Access denied. Insufficient permissions.')
}
// Execute query
try {
const result = await queryFn()
await logOperation(ctx, operation, resource, resourceId, true)
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
await logOperation(ctx, operation, resource, resourceId, false, errorMessage)
throw error
}
}

View File

@@ -0,0 +1,61 @@
// Types
export type {
OperationType,
ResourceType,
AuditLog,
SecurityContext,
AccessRule
} from './types'
// Core functions
export { ACCESS_RULES } from './access-rules'
export { checkAccess } from './check-access'
export { checkRateLimit, clearRateLimit, clearAllRateLimits } from './check-rate-limit'
export { sanitizeInput } from './sanitize-input'
export { logOperation } from './log-operation'
export { executeQuery } from './execute-query'
// Operations
export { getUsers } from './operations/get-users'
export { getUserById } from './operations/get-user-by-id'
export { createUser } from './operations/create-user'
export { updateUser } from './operations/update-user'
export { deleteUser } from './operations/delete-user'
export { verifyCredentials } from './operations/verify-credentials'
// Import all for namespace class
import { ACCESS_RULES } from './access-rules'
import { checkAccess } from './check-access'
import { checkRateLimit, clearRateLimit, clearAllRateLimits } from './check-rate-limit'
import { sanitizeInput } from './sanitize-input'
import { logOperation } from './log-operation'
import { executeQuery } from './execute-query'
import { getUsers } from './operations/get-users'
import { getUserById } from './operations/get-user-by-id'
import { createUser } from './operations/create-user'
import { updateUser } from './operations/update-user'
import { deleteUser } from './operations/delete-user'
import { verifyCredentials } from './operations/verify-credentials'
/**
* SecureDatabase namespace class - groups all secure DB operations as static methods
*/
export class SecureDatabase {
// Core
static ACCESS_RULES = ACCESS_RULES
static checkAccess = checkAccess
static checkRateLimit = checkRateLimit
static clearRateLimit = clearRateLimit
static clearAllRateLimits = clearAllRateLimits
static sanitizeInput = sanitizeInput
static logOperation = logOperation
static executeQuery = executeQuery
// Operations
static getUsers = getUsers
static getUserById = getUserById
static createUser = createUser
static updateUser = updateUser
static deleteUser = deleteUser
static verifyCredentials = verifyCredentials
}

View File

@@ -0,0 +1,38 @@
import type { SecurityContext, OperationType, ResourceType, AuditLog } from './types'
/**
* Log an operation for audit trail
*/
export async function logOperation(
ctx: SecurityContext,
operation: OperationType,
resource: ResourceType,
resourceId: string,
success: boolean,
errorMessage?: string
): Promise<void> {
const log: AuditLog = {
id: `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
userId: ctx.user.id,
username: ctx.user.username,
operation,
resource,
resourceId,
success,
errorMessage,
ipAddress: ctx.ipAddress,
}
try {
// TODO: Replace with proper audit log storage
// For now, just log to console in development
if (process.env.NODE_ENV === 'development') {
console.log('[AUDIT]', log)
}
// In production, this would write to a persistent audit log table
// await Database.addAuditLog(log)
} catch (error) {
console.error('Failed to log operation:', error)
}
}

View File

@@ -0,0 +1,48 @@
import { prisma } from '../../db/prisma'
import type { User } from '../../types/level-types'
import type { SecurityContext } from './types'
import { executeQuery } from './execute-query'
import { sanitizeInput } from './sanitize-input'
/**
* Create a new user with security checks
*/
export async function createUser(
ctx: SecurityContext,
userData: Omit<User, 'id' | 'createdAt'>
): Promise<User> {
const sanitized = sanitizeInput(userData)
return executeQuery(
ctx,
'user',
'CREATE',
async () => {
const user = await prisma.user.create({
data: {
id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
username: sanitized.username,
email: sanitized.email,
role: sanitized.role,
profilePicture: sanitized.profilePicture,
bio: sanitized.bio,
createdAt: BigInt(Date.now()),
tenantId: sanitized.tenantId,
isInstanceOwner: sanitized.isInstanceOwner || false,
},
})
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role as User['role'],
profilePicture: user.profilePicture || undefined,
bio: user.bio || undefined,
createdAt: Number(user.createdAt),
tenantId: user.tenantId || undefined,
isInstanceOwner: user.isInstanceOwner,
}
},
'new_user'
)
}

View File

@@ -0,0 +1,18 @@
import { prisma } from '../../db/prisma'
import type { SecurityContext } from './types'
import { executeQuery } from './execute-query'
/**
* Delete a user with security checks
*/
export async function deleteUser(ctx: SecurityContext, userId: string): Promise<void> {
return executeQuery(
ctx,
'user',
'DELETE',
async () => {
await prisma.user.delete({ where: { id: userId } })
},
userId
)
}

View File

@@ -0,0 +1,31 @@
import { prisma } from '../../db/prisma'
import type { User } from '../../types/level-types'
import type { SecurityContext } from './types'
import { executeQuery } from './execute-query'
/**
* Get a user by ID with security checks
*/
export async function getUserById(ctx: SecurityContext, userId: string): Promise<User | null> {
return executeQuery(
ctx,
'user',
'READ',
async () => {
const user = await prisma.user.findUnique({ where: { id: userId } })
if (!user) return null
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role as User['role'],
profilePicture: user.profilePicture || undefined,
bio: user.bio || undefined,
createdAt: Number(user.createdAt),
tenantId: user.tenantId || undefined,
isInstanceOwner: user.isInstanceOwner,
}
},
userId
)
}

View File

@@ -0,0 +1,30 @@
import { prisma } from '../../db/prisma'
import type { User } from '../../types/level-types'
import type { SecurityContext } from './types'
import { executeQuery } from './execute-query'
/**
* Get all users with security checks
*/
export async function getUsers(ctx: SecurityContext): Promise<User[]> {
return executeQuery(
ctx,
'user',
'READ',
async () => {
const users = await prisma.user.findMany()
return users.map(u => ({
id: u.id,
username: u.username,
email: u.email,
role: u.role as User['role'],
profilePicture: u.profilePicture || undefined,
bio: u.bio || undefined,
createdAt: Number(u.createdAt),
tenantId: u.tenantId || undefined,
isInstanceOwner: u.isInstanceOwner,
}))
},
'all_users'
)
}

View File

@@ -0,0 +1,47 @@
import { prisma } from '../../db/prisma'
import type { User } from '../../types/level-types'
import type { SecurityContext } from './types'
import { executeQuery } from './execute-query'
import { sanitizeInput } from './sanitize-input'
/**
* Update a user with security checks
*/
export async function updateUser(
ctx: SecurityContext,
userId: string,
updates: Partial<User>
): Promise<User> {
const sanitized = sanitizeInput(updates)
return executeQuery(
ctx,
'user',
'UPDATE',
async () => {
const user = await prisma.user.update({
where: { id: userId },
data: {
username: sanitized.username,
email: sanitized.email,
role: sanitized.role,
profilePicture: sanitized.profilePicture,
bio: sanitized.bio,
tenantId: sanitized.tenantId,
},
})
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role as User['role'],
profilePicture: user.profilePicture || undefined,
bio: user.bio || undefined,
createdAt: Number(user.createdAt),
tenantId: user.tenantId || undefined,
isInstanceOwner: user.isInstanceOwner,
}
},
userId
)
}

View File

@@ -0,0 +1,37 @@
import { prisma } from '../../db/prisma'
import type { SecurityContext } from './types'
import { executeQuery } from './execute-query'
import { sanitizeInput } from './sanitize-input'
/**
* Verify user credentials with security checks
*/
export async function verifyCredentials(
ctx: SecurityContext,
username: string,
password: string
): Promise<boolean> {
const sanitizedUsername = sanitizeInput(username)
return executeQuery(
ctx,
'credential',
'READ',
async () => {
const credential = await prisma.credential.findUnique({
where: { username: sanitizedUsername },
})
if (!credential) return false
const encoder = new TextEncoder()
const data = encoder.encode(password)
const hashBuffer = await crypto.subtle.digest('SHA-512', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
return hashHex === credential.passwordHash
},
sanitizedUsername
)
}