Security Model - gabrielmaialva33/innkeeper GitHub Wiki
Comprehensive security architecture and implementation guide for Innkeeper
Innkeeper implements a robust, multi-layered security model designed to protect sensitive hotel and guest data while maintaining compliance with industry standards. The security architecture follows defense-in-depth principles, incorporating multiple security layers from network to application level.
Key Security Principles:
- Zero Trust Architecture - Never trust, always verify
- Defense in Depth - Multiple security layers
- Principle of Least Privilege - Minimal access rights
- Data Protection by Design - Security built into every component
- Compliance Ready - GDPR, PCI DSS, and industry standards
graph TB
subgraph "External Layer"
INTERNET[Internet]
CDN[CDN/WAF]
LB[Load Balancer]
end
subgraph "Network Security"
FIREWALL[Firewall]
VPN[VPN Gateway]
IDS[Intrusion Detection]
end
subgraph "Application Security"
AUTH[Authentication]
AUTHZ[Authorization]
RBAC[Role-Based Access Control]
TENANT[Tenant Isolation]
end
subgraph "Data Security"
ENCRYPT[Encryption at Rest]
TLS[TLS in Transit]
BACKUP[Secure Backups]
AUDIT[Audit Logging]
end
subgraph "Infrastructure Security"
CONTAINER[Container Security]
SECRETS[Secret Management]
MONITOR[Security Monitoring]
INCIDENT[Incident Response]
end
INTERNET --> CDN
CDN --> LB
LB --> FIREWALL
FIREWALL --> AUTH
AUTH --> AUTHZ
AUTHZ --> RBAC
RBAC --> TENANT
TENANT --> ENCRYPT
ENCRYPT --> AUDIT
Innkeeper supports multiple authentication methods:
// Authentication configuration
export default defineConfig({
default: 'web',
guards: {
web: {
driver: 'session',
provider: 'users',
},
api: {
driver: 'access_tokens',
provider: 'users',
},
},
providers: {
users: {
driver: 'lucid',
identifierKey: 'id',
uids: ['email', 'username'],
model: () => import('#models/user'),
},
},
})
// User model with token support
export default class User extends BaseModel {
static accessTokens = DbAccessTokensProvider.forModel(User)
static refreshTokens = DbAccessTokensProvider.forModel(User, {
type: 'refresh_token',
expiresIn: '3d',
})
// Token abilities for API access
async createApiToken(abilities: string[] = ['*']) {
return await User.accessTokens.create(this, abilities, {
expiresIn: '30d'
})
}
}
// Secure password handling
export default class User extends BaseModel {
@beforeSave()
static async hashUserPassword(user: User) {
if (user.$dirty.password && !hash.isValidHash(user.password)) {
user.password = await hash.make(user.password)
}
}
// Password validation rules
static passwordRules = {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
preventCommonPasswords: true,
}
}
Innkeeper implements a flexible RBAC system with granular permissions:
erDiagram
users ||--o{ user_roles : has
roles ||--o{ user_roles : assigned_to
roles ||--o{ role_permissions : has
permissions ||--o{ role_permissions : granted_to
users ||--o{ user_permissions : has_direct
permissions ||--o{ user_permissions : granted_directly
users {
int id PK
string email
string full_name
int organization_id FK
}
roles {
int id PK
string name
string slug
string description
}
permissions {
int id PK
string name
string resource
string action
string context
}
// Permission model with granular control
export default class Permission extends BaseModel {
@column()
declare resource: string // e.g., 'hotel', 'reservation', 'guest'
@column()
declare action: string // e.g., 'create', 'read', 'update', 'delete'
@column()
declare context: string // e.g., 'own', 'organization', 'any'
@beforeCreate()
static async generateName(permission: Permission) {
if (!permission.name) {
const context = permission.context || 'any'
permission.name = `${permission.resource}.${permission.action}.${context}`
}
}
}
// Permission-based authorization
export default class PermissionMiddleware {
async handle(
{auth, response}: HttpContext,
next: NextFn,
options: { permission: string }
) {
const user = auth.user!
// Check if user has required permission
const hasPermission = await this.checkPermission(user, options.permission)
if (!hasPermission) {
return response.forbidden({
error: 'Insufficient permissions',
required: options.permission
})
}
return next()
}
private async checkPermission(user: User, permissionName: string): Promise<boolean> {
// Check direct user permissions
const directPermission = await user
.related('permissions')
.query()
.where('name', permissionName)
.where('granted', true)
.first()
if (directPermission) return true
// Check role-based permissions
const rolePermissions = await user
.related('roles')
.query()
.preload('permissions', (query) => {
query.where('name', permissionName)
})
return rolePermissions.some(role => role.permissions.length > 0)
}
}
// Tenant context middleware for data isolation
export default class TenantContextMiddleware {
async handle({auth, response}: HttpContext, next: NextFn) {
try {
const organizationId = auth.user?.organization_id
if (!organizationId) {
return await next()
}
// Set PostgreSQL session variable for RLS
await db.rawQuery('SELECT set_current_organization(?)', [organizationId])
return await next()
} catch (error) {
return response.status(500).json({
error: 'Failed to set tenant context'
})
}
}
}
-- Enable RLS on tenant-specific tables
ALTER TABLE hotels
ENABLE ROW LEVEL SECURITY;
ALTER TABLE reservations
ENABLE ROW LEVEL SECURITY;
ALTER TABLE guests
ENABLE ROW LEVEL SECURITY;
-- Create tenant isolation policies
CREATE POLICY tenant_isolation_policy ON hotels
FOR ALL
TO application_role
USING (organization_id = current_setting('app.current_organization')::INTEGER);
CREATE POLICY tenant_isolation_policy ON reservations
FOR ALL
TO application_role
USING (organization_id = current_setting('app.current_organization')::INTEGER);
// Validate tenant access in controllers
export default class HotelsController {
async show({params, auth, response}: HttpContext) {
const hotel = await Hotel.find(params.id)
if (!hotel) {
return response.notFound({message: 'Hotel not found'})
}
// Validate tenant ownership
if (hotel.organization_id !== auth.user!.organization_id) {
return response.forbidden({message: 'Access denied'})
}
return hotel
}
}
// Database encryption configuration
export default defineConfig({
connections: {
pg: {
client: 'pg',
connection: {
host: Env.get('DB_HOST'),
port: Env.get('DB_PORT'),
user: Env.get('DB_USER'),
password: Env.get('DB_PASSWORD'),
database: Env.get('DB_DATABASE'),
ssl: {
rejectUnauthorized: false,
ca: Env.get('DB_SSL_CA'),
key: Env.get('DB_SSL_KEY'),
cert: Env.get('DB_SSL_CERT'),
},
},
},
},
})
// PII data encryption
export class DataProtection {
private static encryptionKey = Env.get('ENCRYPTION_KEY')
static async encryptPII(data: string): Promise<string> {
const cipher = crypto.createCipher('aes-256-cbc', this.encryptionKey)
let encrypted = cipher.update(data, 'utf8', 'hex')
encrypted += cipher.final('hex')
return encrypted
}
static async decryptPII(encryptedData: string): Promise<string> {
const decipher = crypto.createDecipher('aes-256-cbc', this.encryptionKey)
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
}
// Guest model with PII protection
export default class Guest extends BaseModel {
@column({
serialize: (value) => DataProtection.encryptPII(value),
consume: (value) => DataProtection.decryptPII(value),
})
declare document_number: string
@column({
serialize: (value) => DataProtection.encryptPII(value),
consume: (value) => DataProtection.decryptPII(value),
})
declare phone: string
}
// Data masking for logs and exports
export class DataMasking {
static maskEmail(email: string): string {
const [username, domain] = email.split('@')
const maskedUsername = username.slice(0, 2) + '*'.repeat(username.length - 2)
return `${maskedUsername}@${domain}`
}
static maskPhone(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
static maskCreditCard(cardNumber: string): string {
return cardNumber.replace(/\d(?=\d{4})/g, '*')
}
}
// API rate limiting configuration
export default defineConfig({
default: 'redis',
stores: {
redis: {
connectionName: 'main',
},
},
// Rate limiting rules by endpoint type
rules: {
api: {
requests: 1000,
duration: '1h',
},
auth: {
requests: 5,
duration: '15m',
},
sensitive: {
requests: 10,
duration: '1h',
},
},
})
// Comprehensive input validation
export class SecurityValidator {
static sanitizeInput = vine.compile(
vine.object({
// Prevent XSS attacks
name: vine.string().trim().escape(),
email: vine.string().email().normalizeEmail(),
phone: vine.string().regex(/^\+?[\d\s\-\(\)]+$/),
// SQL injection prevention (handled by ORM)
search: vine.string().trim().maxLength(100),
// File upload validation
file: vine.file({
size: '10mb',
extnames: ['jpg', 'jpeg', 'png', 'pdf'],
}),
})
)
}
// Secure CORS configuration
export default defineConfig({
enabled: true,
origin: (origin, callback) => {
const allowedOrigins = [
'https://yourdomain.com',
'https://app.yourdomain.com',
]
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400, // 24 hours
})
// Audit logging service
export class AuditService {
async logUserAction(data: AuditLogData): Promise<AuditLog> {
return await AuditLog.create({
user_id: data.userId,
session_id: data.sessionId,
ip_address: data.ipAddress,
user_agent: data.userAgent,
resource: data.resource,
action: data.action,
context: data.context,
resource_id: data.resourceId,
method: data.method,
url: data.url,
request_data: data.requestData,
result: data.result,
response_code: data.responseCode,
metadata: data.metadata,
})
}
async getSecurityAlerts(): Promise<SecurityAlert[]> {
// Detect suspicious activities
const suspiciousLogins = await this.detectSuspiciousLogins()
const failedAttempts = await this.detectFailedAttempts()
const dataExfiltration = await this.detectDataExfiltration()
return [...suspiciousLogins, ...failedAttempts, ...dataExfiltration]
}
}
// Comprehensive audit log model
export default class AuditLog extends BaseModel {
@column()
declare user_id: number
@column()
declare session_id: string
@column()
declare ip_address: string
@column()
declare user_agent: string
@column()
declare resource: string
@column()
declare action: string
@column()
declare context: string
@column()
declare resource_id: number | null
@column()
declare method: string
@column()
declare url: string
@column({
prepare: (value) => JSON.stringify(value),
consume: (value) => JSON.parse(value),
})
declare request_data: Record<string, any>
@column()
declare result: string
@column()
declare response_code: number
@column({
prepare: (value) => JSON.stringify(value),
consume: (value) => JSON.parse(value),
})
declare metadata: Record<string, any>
}
// Security monitoring service
export class SecurityMonitor {
async detectAnomalies(): Promise<SecurityAnomaly[]> {
const anomalies: SecurityAnomaly[] = []
// Detect unusual login patterns
const unusualLogins = await this.detectUnusualLogins()
anomalies.push(...unusualLogins)
// Detect privilege escalation attempts
const privilegeEscalation = await this.detectPrivilegeEscalation()
anomalies.push(...privilegeEscalation)
// Detect data access anomalies
const dataAnomalies = await this.detectDataAccessAnomalies()
anomalies.push(...dataAnomalies)
return anomalies
}
private async detectUnusualLogins(): Promise<SecurityAnomaly[]> {
// Check for logins from new locations
// Check for logins at unusual times
// Check for multiple failed attempts
return []
}
}
graph TB
DETECT[Security Event Detected]
ANALYZE[Analyze Threat Level]
CLASSIFY{Classify Incident}
LOW[Low Priority]
MEDIUM[Medium Priority]
HIGH[High Priority]
CRITICAL[Critical Priority]
LOG[Log Incident]
NOTIFY[Notify Security Team]
ISOLATE[Isolate Affected Systems]
INVESTIGATE[Investigate & Contain]
REMEDIATE[Remediate & Recover]
REVIEW[Post-Incident Review]
DETECT --> ANALYZE
ANALYZE --> CLASSIFY
CLASSIFY --> LOW
CLASSIFY --> MEDIUM
CLASSIFY --> HIGH
CLASSIFY --> CRITICAL
LOW --> LOG
MEDIUM --> NOTIFY
HIGH --> ISOLATE
CRITICAL --> ISOLATE
LOG --> REVIEW
NOTIFY --> INVESTIGATE
ISOLATE --> INVESTIGATE
INVESTIGATE --> REMEDIATE
REMEDIATE --> REVIEW
Threat | Description | Mitigation |
---|---|---|
Spoofing | Identity impersonation | Multi-factor authentication, JWT tokens |
Tampering | Data modification | Input validation, checksums, audit logs |
Repudiation | Denial of actions | Comprehensive audit logging, digital signatures |
Information Disclosure | Unauthorized data access | Encryption, access controls, data masking |
Denial of Service | Service unavailability | Rate limiting, load balancing, monitoring |
Elevation of Privilege | Unauthorized access escalation | RBAC, principle of least privilege |
// Risk assessment framework
export class RiskAssessment {
static assessRisk(threat: Threat): RiskLevel {
const impact = this.calculateImpact(threat)
const likelihood = this.calculateLikelihood(threat)
return this.determineRiskLevel(impact, likelihood)
}
private static calculateImpact(threat: Threat): Impact {
// Consider data sensitivity, business impact, compliance requirements
return threat.dataClassification === 'PII' ? 'HIGH' : 'MEDIUM'
}
private static calculateLikelihood(threat: Threat): Likelihood {
// Consider threat actor capability, attack surface, existing controls
return threat.hasExistingControls ? 'LOW' : 'MEDIUM'
}
}
// Always validate and sanitize inputs
export class SecureController {
async create({request, response}: HttpContext) {
// Validate input
const payload = await request.validateUsing(createValidator)
// Sanitize data
const sanitizedData = this.sanitizeInput(payload)
// Process with sanitized data
const result = await Service.create(sanitizedData)
return response.created(result)
}
}
// Secure error handling - don't expose sensitive information
export class ErrorHandler {
async handle(error: any, ctx: HttpContext) {
// Log full error details securely
logger.error('Application error', {
error: error.message,
stack: error.stack,
user: ctx.auth.user?.id,
ip: ctx.request.ip(),
url: ctx.request.url(),
})
// Return sanitized error to client
if (error.status === 422) {
return ctx.response.status(422).json({
error: 'Validation failed',
details: error.messages // Only validation messages
})
}
// Generic error for security
return ctx.response.status(500).json({
error: 'Internal server error'
})
}
}
// Environment-based security configuration
export class SecurityConfig {
static getConfig() {
return {
// Strong session configuration
session: {
cookieName: 'innkeeper_session',
secure: Env.get('NODE_ENV') === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: '2h',
},
// CSRF protection
csrf: {
enabled: true,
exceptRoutes: ['/api/webhooks/*'],
},
// Content Security Policy
csp: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}
}
}
-
Authentication & Authorization
- Multi-factor authentication enabled
- Strong password policies enforced
- JWT tokens properly configured
- RBAC system implemented and tested
- Session management secure
-
Data Protection
- Encryption at rest enabled
- TLS/SSL properly configured
- PII data encrypted
- Data masking implemented
- Secure backup procedures
-
API Security
- Rate limiting configured
- Input validation comprehensive
- CORS properly configured
- API versioning implemented
- Error handling secure
-
Infrastructure Security
- Firewall rules configured
- VPN access secured
- Container security implemented
- Secret management in place
- Security monitoring active
-
Compliance & Auditing
- Audit logging comprehensive
- Compliance requirements met
- Data retention policies defined
- Incident response plan ready
- Security training completed
// Automated security maintenance
export class SecurityMaintenance {
async performSecurityTasks(): Promise<void> {
// Rotate encryption keys
await this.rotateEncryptionKeys()
// Clean up old audit logs
await this.cleanupAuditLogs()
// Update security policies
await this.updateSecurityPolicies()
// Scan for vulnerabilities
await this.performVulnerabilityScan()
// Generate security reports
await this.generateSecurityReports()
}
private async rotateEncryptionKeys(): Promise<void> {
// Implement key rotation logic
}
private async cleanupAuditLogs(): Promise<void> {
// Remove logs older than retention period
const retentionDays = 365
await AuditLog.query()
.where('created_at', '<', DateTime.now().minus({days: retentionDays}))
.delete()
}
}
// GDPR compliance utilities
export class GDPRCompliance {
async handleDataSubjectRequest(request: DataSubjectRequest): Promise<void> {
switch (request.type) {
case 'access':
await this.provideDataAccess(request.userId)
break
case 'rectification':
await this.rectifyData(request.userId, request.corrections)
break
case 'erasure':
await this.eraseData(request.userId)
break
case 'portability':
await this.exportData(request.userId)
break
}
}
private async eraseData(userId: number): Promise<void> {
// Anonymize or delete personal data
await Guest.query()
.where('user_id', userId)
.update({
first_name: 'DELETED',
last_name: 'DELETED',
email: '[email protected]',
phone: null,
document_number: null,
})
}
}
// PCI DSS compliance for payment data
export class PCICompliance {
static validateCardData(cardData: CardData): boolean {
// Never store full PAN
if (cardData.number && cardData.number.length > 6) {
throw new Error('Full card number storage not allowed')
}
// Validate CVV is not stored
if (cardData.cvv) {
throw new Error('CVV storage not allowed')
}
return true
}
static maskCardNumber(cardNumber: string): string {
// Show only first 6 and last 4 digits
return cardNumber.replace(/(\d{6})\d+(\d{4})/, '$1******$2')
}
}
- System Architecture - Overall system security design
- Multi-Tenant Architecture - Tenant security isolation
- API Documentation - API security implementation
- Configuration Guide - Security configuration settings
- Database Schema - Data security structure
- 📖 Documentation: Wiki Home
- 🐛 Issues: GitHub Issues
- 💬 Community: GitHub Discussions
- 📧 Support: Contact the development team
← Previous: Multi-Tenant Architecture | Wiki Home | Next: API Documentation →