Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 41 additions & 9 deletions backend/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,26 +116,58 @@ export async function authenticateToken(req: AuthRequest, res: Response, next: N
* Deve ser usado APΓ“S authenticateToken
*/
export async function requireAdmin(req: AuthRequest, res: Response, next: NextFunction) {
if (!req.user) {
if (!req.user || !req.userId) {
return res.status(401).json({ error: 'UsuΓ‘rio nΓ£o autenticado' });
}

// Lista de emails de admin (gustavo.caetano@gmail.com + variΓ‘vel de ambiente)
const defaultAdminEmails = ['gustavo.caetano@gmail.com'];
const envAdminEmails = (process.env.ADMIN_EMAILS || '').split(',').map(e => e.trim().toLowerCase()).filter(e => e);
const adminEmails = [...defaultAdminEmails, ...envAdminEmails].map(e => e.toLowerCase());
// 1. Check Environment Variable (Config-based Admin)
const envAdminEmails = (process.env.ADMIN_EMAILS || '')
.split(',')
.map(e => e.trim().toLowerCase())
.filter(e => e);

const userEmail = req.user.email?.toLowerCase();

if (!userEmail || !adminEmails.includes(userEmail)) {
if (userEmail && envAdminEmails.includes(userEmail)) {
console.log(`[AUTH] βœ… Admin access granted (ENV) for ${userEmail}`);
return next();
}

try {
// 2. Check Database Roles (RBAC)
// Check if user has 'admin' role in user_roles table
const hasAdminRole = await queryOne(
`SELECT 1 FROM user_roles WHERE user_id = $1 AND role = 'admin'`,
[req.userId]
);

if (hasAdminRole) {
console.log(`[AUTH] βœ… Admin access granted (ROLE) for ${userEmail}`);
return next();
}

// 3. Check User Flag (Legacy/Alternative)
// Check if user has is_admin flag in users table
const isUserAdmin = await queryOne(
`SELECT 1 FROM users WHERE id = $1 AND is_admin = true`,
[req.userId]
);

if (isUserAdmin) {
console.log(`[AUTH] βœ… Admin access granted (FLAG) for ${userEmail}`);
return next();
}

// If none of the above, deny access
return res.status(403).json({
error: 'Acesso negado. Apenas administradores podem executar esta aΓ§Γ£o.',
code: 'ADMIN_REQUIRED'
});
}

console.log(`[AUTH] βœ… Admin access granted for ${userEmail}`);
next();
} catch (error) {
console.error('[AUTH] Error checking admin status:', error);
return res.status(500).json({ error: 'Erro ao verificar permissΓ΅es' });
}
}

// Alias para compatibilidade com Password Vault
Expand Down
97 changes: 97 additions & 0 deletions backend/src/tests/auth-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { requireAdmin, AuthRequest } from '../middleware/auth';
import { queryOne } from '../services/database';
import { Response, NextFunction } from 'express';

// Mock database
jest.mock('../services/database', () => ({
queryOne: jest.fn(),
query: jest.fn(),
}));

describe('requireAdmin Middleware Security Fix', () => {
let mockReq: Partial<AuthRequest>;
let mockRes: Partial<Response>;
let mockNext: jest.Mock;

beforeEach(() => {
jest.clearAllMocks();
mockReq = {
user: {
id: 'user-123',
email: 'test@example.com',
},
userId: 'user-123'
};
mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
mockNext = jest.fn();
// Reset env vars
process.env.ADMIN_EMAILS = '';
});

it('should deny access if user is not authenticated', async () => {
mockReq.user = undefined;
await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction);
expect(mockRes.status).toHaveBeenCalledWith(401);
expect(mockNext).not.toHaveBeenCalled();
});

it('should deny access for regular user', async () => {
(queryOne as jest.Mock).mockResolvedValue(null); // Not found in DB checks

await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction);

expect(mockRes.status).toHaveBeenCalledWith(403);
expect(mockNext).not.toHaveBeenCalled();
});

it('should GRANT access if user email is in ADMIN_EMAILS env var', async () => {
process.env.ADMIN_EMAILS = 'test@example.com';

await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction);

expect(mockNext).toHaveBeenCalled();
expect(mockRes.status).not.toHaveBeenCalled();
});

it('should DENY access for hardcoded "gustavo.caetano@gmail.com" if not in DB or Env', async () => {
mockReq.user!.email = 'gustavo.caetano@gmail.com';
(queryOne as jest.Mock).mockResolvedValue(null); // Not in DB

await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction);

// If the vulnerability is present, this expectation will FAIL (it would call next())
expect(mockRes.status).toHaveBeenCalledWith(403);
expect(mockNext).not.toHaveBeenCalled();
});

it('should GRANT access if user has admin role in user_roles', async () => {
// Mock finding admin role
(queryOne as jest.Mock).mockResolvedValueOnce({ 1: 1 }); // Found row

await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction);

expect(queryOne).toHaveBeenCalledWith(
expect.stringContaining('user_roles'),
expect.arrayContaining(['user-123'])
);
expect(mockNext).toHaveBeenCalled();
});

it('should GRANT access if user has is_admin=true in users table', async () => {
// Mock NOT finding in user_roles first
(queryOne as jest.Mock).mockResolvedValueOnce(null);
// Mock finding is_admin in users
(queryOne as jest.Mock).mockResolvedValueOnce({ is_admin: true });

await requireAdmin(mockReq as AuthRequest, mockRes as Response, mockNext as NextFunction);

expect(queryOne).toHaveBeenCalledWith(
expect.stringContaining('users'),
expect.arrayContaining(['user-123'])
);
expect(mockNext).toHaveBeenCalled();
});
});
Loading