mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-08 09:54:02 +00:00
Update API documentation and UI to improve clarity and add role-based access control.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/994ee92d-6a6d-4ca4-99c0-fe0dbc7f160b.jpg
This commit is contained in:
+18
-6
@@ -110,12 +110,19 @@ export function setupAuth(app: Express) {
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
// Get the default role if role ID isn't specified
|
||||
let roleId = req.body.roleId;
|
||||
if (!roleId) {
|
||||
const defaultRole = await storage.getDefaultRole();
|
||||
roleId = defaultRole?.id;
|
||||
}
|
||||
|
||||
const user = await storage.createUser({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email: req.body.email,
|
||||
fullName: req.body.fullName,
|
||||
role: req.body.role || "user",
|
||||
roleId: roleId,
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
@@ -132,7 +139,7 @@ export function setupAuth(app: Express) {
|
||||
|
||||
// Login endpoint
|
||||
app.post("/api/login", (req, res, next) => {
|
||||
passport.authenticate("local", (err, user, info) => {
|
||||
passport.authenticate("local", (err: any, user: any, info: any) => {
|
||||
if (err) return next(err);
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: info?.message || "Authentication failed" });
|
||||
@@ -175,25 +182,30 @@ export function setupAuth(app: Express) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, expiresAt, permissions } = req.body;
|
||||
const { name, expiresAt, roleId, customPermissions } = req.body;
|
||||
if (!name) {
|
||||
return res.status(400).json({ message: "Token name is required" });
|
||||
}
|
||||
|
||||
// Create JWT token with user ID and optional permissions
|
||||
const token = jwt.sign(
|
||||
{
|
||||
sub: req.user.id,
|
||||
permissions
|
||||
customPermissions
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresAt: expiresAt ? new Date(expiresAt) : undefined }
|
||||
{
|
||||
expiresIn: expiresAt ? Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000) : '365d'
|
||||
}
|
||||
);
|
||||
|
||||
// Store the token in the database
|
||||
const apiToken = storage.createApiToken({
|
||||
name,
|
||||
token,
|
||||
userId: req.user.id,
|
||||
permissions: permissions || {},
|
||||
roleId: roleId || null,
|
||||
customPermissions: customPermissions || null,
|
||||
expiresAt: expiresAt ? new Date(expiresAt) : null,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "./db";
|
||||
import { roles, rolePermissions, users, apiTokens } from "@shared/schema";
|
||||
import { eq, or, and, inArray } from "drizzle-orm";
|
||||
|
||||
// Types for role-based access control
|
||||
export type RequireAuthOptions = {
|
||||
allowApiToken?: boolean;
|
||||
};
|
||||
|
||||
export type RequirePermissionOptions = RequireAuthOptions & {
|
||||
anyOf?: string[];
|
||||
allOf?: string[];
|
||||
};
|
||||
|
||||
// Default middleware that verifies a user is authenticated
|
||||
export function requireAuth(options: RequireAuthOptions = {}) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// Check for session authentication
|
||||
if (req.isAuthenticated()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check for API token authentication if allowed
|
||||
if (options.allowApiToken) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
// Lookup the token
|
||||
const [apiToken] = await db.select()
|
||||
.from(apiTokens)
|
||||
.where(eq(apiTokens.token, token))
|
||||
.limit(1);
|
||||
|
||||
if (!apiToken) {
|
||||
return res.status(401).json({ message: "Invalid API token" });
|
||||
}
|
||||
|
||||
// Check if token has expired
|
||||
if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) {
|
||||
return res.status(401).json({ message: "API token has expired" });
|
||||
}
|
||||
|
||||
// Set token info on the request
|
||||
req.user = {
|
||||
id: apiToken.userId,
|
||||
username: '',
|
||||
password: '',
|
||||
fullName: null,
|
||||
email: null,
|
||||
createdAt: null,
|
||||
// Add tokenId for identifying which token was used
|
||||
tokenId: apiToken.id,
|
||||
// Add roleId for permission checking
|
||||
roleId: apiToken.roleId,
|
||||
// Store custom permissions if available
|
||||
customPermissions: (Array.isArray(apiToken.customPermissions) ?
|
||||
apiToken.customPermissions :
|
||||
(apiToken.customPermissions ? JSON.parse(String(apiToken.customPermissions)) : [])) as string[]
|
||||
};
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
console.error("API token authentication error:", error);
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not authenticated through any method
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
};
|
||||
}
|
||||
|
||||
// Middleware for checking specific permissions
|
||||
export function requirePermission(permission: string, options: RequirePermissionOptions = {}) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// First ensure the user is authenticated
|
||||
const authMiddleware = requireAuth(options);
|
||||
|
||||
authMiddleware(req, res, async () => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
// Get the permissions for the user's role
|
||||
let hasPermission = false;
|
||||
|
||||
// Check for API token custom permissions first if present
|
||||
if (req.user?.tokenId && Array.isArray(req.user?.customPermissions)) {
|
||||
// For API tokens with custom permissions
|
||||
const customPermissions = req.user.customPermissions;
|
||||
if (customPermissions.includes(permission) ||
|
||||
options.anyOf?.some(p => customPermissions.includes(p))) {
|
||||
hasPermission = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no permission yet, check role-based permissions
|
||||
if (!hasPermission && req.user.roleId) {
|
||||
// Get permissions assigned to the role
|
||||
const rolePerms = await db.select()
|
||||
.from(rolePermissions)
|
||||
.where(eq(rolePermissions.roleId, req.user.roleId));
|
||||
|
||||
const userPermissions = rolePerms.map(rp => rp.permission);
|
||||
|
||||
// Check for the specific permission or any of the optional permissions
|
||||
if (userPermissions.includes(permission) ||
|
||||
options.anyOf?.some(p => userPermissions.includes(p))) {
|
||||
hasPermission = true;
|
||||
}
|
||||
|
||||
// If allOf is specified, ensure all required permissions are present
|
||||
if (options.allOf && options.allOf.length > 0) {
|
||||
hasPermission = options.allOf.every(p => userPermissions.includes(p));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user has the admin:system permission which grants all access
|
||||
if (!hasPermission && req.user.roleId) {
|
||||
const adminPerm = await db.select()
|
||||
.from(rolePermissions)
|
||||
.where(and(
|
||||
eq(rolePermissions.roleId, req.user.roleId),
|
||||
eq(rolePermissions.permission, "admin:system")
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (adminPerm.length > 0) {
|
||||
hasPermission = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPermission) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
} catch (error) {
|
||||
console.error("Permission check error:", error);
|
||||
return res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Convenience middleware for requiring administrative access
|
||||
export function requireAdmin() {
|
||||
return requirePermission("admin:system");
|
||||
}
|
||||
|
||||
// Get permissions for a user
|
||||
export async function getUserPermissions(userId: number): Promise<string[]> {
|
||||
const [user] = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user || !user.roleId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rolePerms = await db.select()
|
||||
.from(rolePermissions)
|
||||
.where(eq(rolePermissions.roleId, user.roleId));
|
||||
|
||||
return rolePerms.map(rp => rp.permission);
|
||||
}
|
||||
|
||||
// Initialize on server startup to ensure default roles
|
||||
export async function initializeRBAC() {
|
||||
try {
|
||||
// Check for existing data before doing anything
|
||||
const [adminRole] = await db.select()
|
||||
.from(roles)
|
||||
.where(eq(roles.name, "admin"))
|
||||
.limit(1);
|
||||
|
||||
if (adminRole) {
|
||||
console.log("RBAC already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log("RBAC initialization skipped - should be handled by updateSchema.ts");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize RBAC:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+21
-16
@@ -3,8 +3,14 @@ import { createServer, type Server } from "http";
|
||||
import { setupAuth } from "./auth";
|
||||
import { setupSwagger } from "./swagger";
|
||||
import { storage } from "./storage";
|
||||
import { apiQuerySchema } from "@shared/schema";
|
||||
import { apiQuerySchema, PERMISSIONS } from "@shared/schema";
|
||||
import { ZodError } from "zod";
|
||||
import {
|
||||
requireAuth,
|
||||
requirePermission,
|
||||
requireAdmin,
|
||||
initializeRBAC
|
||||
} from "./authorization";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Setup authentication
|
||||
@@ -13,6 +19,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Setup Swagger documentation
|
||||
setupSwagger(app);
|
||||
|
||||
// Initialize Role Based Access Control system
|
||||
await initializeRBAC();
|
||||
|
||||
// Error handler for Zod validation errors
|
||||
const handleZodError = (err: ZodError, res: Response) => {
|
||||
return res.status(400).json({
|
||||
@@ -33,14 +42,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
};
|
||||
|
||||
// Middleware to check admin role
|
||||
const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.isAuthenticated() || req.user.role !== "admin") {
|
||||
return res.status(403).json({ message: "Access denied: Admin role required" });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-connections:
|
||||
@@ -49,6 +50,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
* tags: [LDAP Connections]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: A list of LDAP connections
|
||||
@@ -60,13 +62,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
* $ref: '#/components/schemas/LdapConnection'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 403:
|
||||
* $ref: '#/components/responses/ForbiddenError'
|
||||
*/
|
||||
app.get("/api/ldap-connections", async (req, res, next) => {
|
||||
app.get("/api/ldap-connections", requirePermission(PERMISSIONS.VIEW_LDAP_CONNECTIONS, { allowApiToken: true }), async (req, res, next) => {
|
||||
try {
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
const connections = await storage.listLdapConnections();
|
||||
|
||||
// Hide sensitive fields like password
|
||||
@@ -360,8 +360,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
|
||||
// Only allow users to delete their own tokens unless they're admin
|
||||
if (token.userId !== req.user.id && req.user.role !== "admin") {
|
||||
return res.status(403).json({ message: "Forbidden: You cannot delete tokens that don't belong to you" });
|
||||
if (token.userId !== req.user.id) {
|
||||
// Get the user's role
|
||||
const userRole = await storage.getRole(req.user.roleId!);
|
||||
|
||||
if (userRole?.name !== "admin") {
|
||||
return res.status(403).json({ message: "Forbidden: You cannot delete tokens that don't belong to you" });
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await storage.deleteApiToken(parseInt(req.params.id));
|
||||
|
||||
+24
-8
@@ -3,8 +3,9 @@ import {
|
||||
LdapConnection, InsertLdapConnection,
|
||||
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
|
||||
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
|
||||
AdDomain, InsertAdDomain,
|
||||
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains
|
||||
AdDomain, InsertAdDomain, Role,
|
||||
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
|
||||
roles
|
||||
} from "@shared/schema";
|
||||
import session from "express-session";
|
||||
import createMemoryStore from "memorystore";
|
||||
@@ -32,7 +33,11 @@ export interface IStorage {
|
||||
updateUser(id: number, user: Partial<User>): Promise<User | undefined>;
|
||||
deleteUser(id: number): Promise<boolean>;
|
||||
listUsers(): Promise<User[]>;
|
||||
|
||||
|
||||
// Role management
|
||||
getRole(id: number): Promise<Role | undefined>;
|
||||
getDefaultRole(): Promise<Role | undefined>;
|
||||
|
||||
// API Token management
|
||||
getApiToken(id: number): Promise<ApiToken | undefined>;
|
||||
getApiTokenByToken(token: string): Promise<ApiToken | undefined>;
|
||||
@@ -126,6 +131,17 @@ export class DatabaseStorage implements IStorage {
|
||||
async listUsers(): Promise<User[]> {
|
||||
return db.select().from(users);
|
||||
}
|
||||
|
||||
// Role management
|
||||
async getRole(id: number): Promise<Role | undefined> {
|
||||
const result = await db.select().from(roles).where(eq(roles.id, id));
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
async getDefaultRole(): Promise<Role | undefined> {
|
||||
const result = await db.select().from(roles).where(eq(roles.isDefault, true));
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
// API Token management
|
||||
async getApiToken(id: number): Promise<ApiToken | undefined> {
|
||||
@@ -210,7 +226,7 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
return users.map(user => {
|
||||
const result: any = { id: user.id };
|
||||
properties.forEach(prop => {
|
||||
properties.forEach((prop: string) => {
|
||||
if ((user as any)[prop] !== undefined) {
|
||||
result[prop] = (user as any)[prop];
|
||||
}
|
||||
@@ -253,7 +269,7 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
return groups.map(group => {
|
||||
const result: any = { id: group.id };
|
||||
properties.forEach(prop => {
|
||||
properties.forEach((prop: string) => {
|
||||
if ((group as any)[prop] !== undefined) {
|
||||
result[prop] = (group as any)[prop];
|
||||
}
|
||||
@@ -296,7 +312,7 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
return orgUnits.map(ou => {
|
||||
const result: any = { id: ou.id };
|
||||
properties.forEach(prop => {
|
||||
properties.forEach((prop: string) => {
|
||||
if ((ou as any)[prop] !== undefined) {
|
||||
result[prop] = (ou as any)[prop];
|
||||
}
|
||||
@@ -339,7 +355,7 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
return computers.map(computer => {
|
||||
const result: any = { id: computer.id };
|
||||
properties.forEach(prop => {
|
||||
properties.forEach((prop: string) => {
|
||||
if ((computer as any)[prop] !== undefined) {
|
||||
result[prop] = (computer as any)[prop];
|
||||
}
|
||||
@@ -382,7 +398,7 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
return domains.map(domain => {
|
||||
const result: any = { id: domain.id };
|
||||
properties.forEach(prop => {
|
||||
properties.forEach((prop: string) => {
|
||||
if ((domain as any)[prop] !== undefined) {
|
||||
result[prop] = (domain as any)[prop];
|
||||
}
|
||||
|
||||
+10
-1
@@ -53,7 +53,7 @@ const swaggerOptions = {
|
||||
name: { type: "string" },
|
||||
token: { type: "string" },
|
||||
userId: { type: "integer" },
|
||||
permissions: { type: "object" },
|
||||
customPermissions: { type: "array", items: { type: "string" } },
|
||||
expiresAt: { type: "string", format: "date-time" },
|
||||
createdAt: { type: "string", format: "date-time" },
|
||||
},
|
||||
@@ -240,9 +240,18 @@ const swaggerOptions = {
|
||||
const swaggerSpec = swaggerJsdoc(swaggerOptions);
|
||||
|
||||
export function setupSwagger(app: Express) {
|
||||
// Mount at both paths for backward compatibility
|
||||
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
|
||||
// Provide the JSON spec at multiple paths
|
||||
app.get("/api/swagger.json", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.send(swaggerSpec);
|
||||
});
|
||||
|
||||
app.get("/api-docs/swagger.json", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.send(swaggerSpec);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user