mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-27 19:17:07 +00:00
Fix authentication errors by improving authorization middleware and Docker deployment.
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/29c4139d-db30-4851-8296-3b0c9c5c6466.jpg
This commit is contained in:
@@ -1,213 +0,0 @@
|
|||||||
# Docker Deployment Instructions
|
|
||||||
|
|
||||||
This document describes how to deploy the Active Directory Management API using Docker.
|
|
||||||
|
|
||||||
## Dockerfile
|
|
||||||
|
|
||||||
Below is the updated Dockerfile that includes support for all the enhanced features:
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM node:20-slim AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy the rest of the source code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application (optimized for production)
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-slim AS runner
|
|
||||||
|
|
||||||
# Install necessary system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set environment to production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
# Copy package files and install production dependencies
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --production
|
|
||||||
|
|
||||||
# Copy build artifacts from the builder stage
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
|
|
||||||
# Copy schema files for Drizzle
|
|
||||||
COPY ./shared/schema.ts ./shared/
|
|
||||||
COPY ./drizzle.config.ts ./
|
|
||||||
|
|
||||||
# Create a directory for logs
|
|
||||||
RUN mkdir -p /app/logs
|
|
||||||
|
|
||||||
# Set default environment variables (these should be overridden at runtime)
|
|
||||||
ENV DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
|
|
||||||
ENV REDIS_URL=redis://redis:6379
|
|
||||||
ENV SESSION_SECRET=changeme
|
|
||||||
|
|
||||||
# Set rate limiting configuration
|
|
||||||
ENV RATE_LIMIT_WINDOW_MS=900000
|
|
||||||
ENV RATE_LIMIT_MAX_REQUESTS=100
|
|
||||||
|
|
||||||
# Set compression level (1-9, where 9 is maximum compression)
|
|
||||||
ENV COMPRESSION_LEVEL=6
|
|
||||||
|
|
||||||
# Set logging level (debug, info, warn, error)
|
|
||||||
ENV LOG_LEVEL=info
|
|
||||||
|
|
||||||
# Expose the port the app runs on
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Health check to ensure the application is running properly
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:5000/api/health || exit 1
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
CMD ["node", "dist/index.js"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Compose
|
|
||||||
|
|
||||||
For deploying the full stack with PostgreSQL and Redis, create a `docker-compose.yml` file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- SESSION_SECRET=your_secure_session_secret
|
|
||||||
- RATE_LIMIT_WINDOW_MS=900000
|
|
||||||
- RATE_LIMIT_MAX_REQUESTS=100
|
|
||||||
- COMPRESSION_LEVEL=6
|
|
||||||
- LOG_LEVEL=info
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
- redis
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:15
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=postgres
|
|
||||||
- POSTGRES_PASSWORD=postgres
|
|
||||||
- POSTGRES_DB=postgres
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
command: redis-server --appendonly yes
|
|
||||||
volumes:
|
|
||||||
- redis-data:/data
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
redis-data:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
The application supports the following environment variables:
|
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `NODE_ENV` | Application environment | `production` |
|
|
||||||
| `DATABASE_URL` | PostgreSQL connection string | `postgres://postgres:postgres@postgres:5432/postgres` |
|
|
||||||
| `REDIS_URL` | Redis connection string | `redis://redis:6379` |
|
|
||||||
| `SESSION_SECRET` | Secret for session encryption | `changeme` (replace in production!) |
|
|
||||||
| `RATE_LIMIT_WINDOW_MS` | Rate limiting window in milliseconds | `900000` (15 minutes) |
|
|
||||||
| `RATE_LIMIT_MAX_REQUESTS` | Maximum requests per window | `100` |
|
|
||||||
| `COMPRESSION_LEVEL` | Response compression level (1-9) | `6` |
|
|
||||||
| `LOG_LEVEL` | Logging level | `info` |
|
|
||||||
|
|
||||||
## Deployment Instructions
|
|
||||||
|
|
||||||
1. Create the Dockerfile and docker-compose.yml as specified above
|
|
||||||
2. Build and start the containers:
|
|
||||||
```
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
3. Run database migrations:
|
|
||||||
```
|
|
||||||
docker-compose exec app npm run db:push
|
|
||||||
```
|
|
||||||
4. Access the application at http://localhost:5000
|
|
||||||
5. API documentation is available at http://localhost:5000/api/docs
|
|
||||||
|
|
||||||
## Health Checks
|
|
||||||
|
|
||||||
The application includes a health check endpoint at `/api/health` that returns HTTP 200 when the application is running normally. The Docker container is configured to use this endpoint for health checks.
|
|
||||||
|
|
||||||
## Scaling Considerations
|
|
||||||
|
|
||||||
- The application can be horizontally scaled by adding more instances of the app service
|
|
||||||
- For high-availability, consider using a managed PostgreSQL service instead of the containerized version
|
|
||||||
- For production, use a proper Redis cluster or managed Redis service
|
|
||||||
- Consider using a reverse proxy like Nginx or a load balancer in front of multiple app instances
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
- Always replace the default `SESSION_SECRET` with a secure random string
|
|
||||||
- Use appropriate firewall rules to restrict access to PostgreSQL and Redis
|
|
||||||
- Consider setting up SSL/TLS for production deployments
|
|
||||||
- Adjust rate limiting parameters based on your expected traffic
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- If the application fails to start, check the logs:
|
|
||||||
```
|
|
||||||
docker-compose logs app
|
|
||||||
```
|
|
||||||
- For database connection issues:
|
|
||||||
```
|
|
||||||
docker-compose logs postgres
|
|
||||||
```
|
|
||||||
- For Redis connection issues:
|
|
||||||
```
|
|
||||||
docker-compose logs redis
|
|
||||||
```
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
FROM node:20-slim AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy the rest of the source code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application (optimized for production)
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-slim AS runner
|
|
||||||
|
|
||||||
# Install necessary system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set environment to production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
# Copy package files and install production dependencies
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --production
|
|
||||||
|
|
||||||
# Copy build artifacts from the builder stage
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
|
|
||||||
# Copy schema files for Drizzle
|
|
||||||
COPY ./shared/schema.ts ./shared/
|
|
||||||
COPY ./drizzle.config.ts ./
|
|
||||||
|
|
||||||
# Create a directory for logs
|
|
||||||
RUN mkdir -p /app/logs
|
|
||||||
|
|
||||||
# Set default environment variables (these should be overridden at runtime)
|
|
||||||
ENV DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
|
|
||||||
ENV REDIS_URL=redis://redis:6379
|
|
||||||
ENV SESSION_SECRET=changeme
|
|
||||||
|
|
||||||
# Set rate limiting configuration
|
|
||||||
ENV RATE_LIMIT_WINDOW_MS=900000
|
|
||||||
ENV RATE_LIMIT_MAX_REQUESTS=100
|
|
||||||
|
|
||||||
# Set compression level (1-9, where 9 is maximum compression)
|
|
||||||
ENV COMPRESSION_LEVEL=6
|
|
||||||
|
|
||||||
# Set logging level (debug, info, warn, error)
|
|
||||||
ENV LOG_LEVEL=info
|
|
||||||
|
|
||||||
# Expose the port the app runs on
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Add startup script with health check
|
|
||||||
COPY ./docker-entrypoint.sh /
|
|
||||||
RUN chmod +x /docker-entrypoint.sh
|
|
||||||
|
|
||||||
# Health check to ensure the application is running properly
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:5000/api/health || exit 1
|
|
||||||
|
|
||||||
# Start the application using the entrypoint script
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
|
||||||
CMD ["node", "dist/index.js"]
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
FROM node:20-slim AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy the rest of the source code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application (optimized for production)
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-slim AS runner
|
|
||||||
|
|
||||||
# Install necessary system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set environment to production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
# Copy package files and install production dependencies
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --production
|
|
||||||
|
|
||||||
# Copy build artifacts from the builder stage
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
|
|
||||||
# Copy schema files for Drizzle
|
|
||||||
COPY ./shared/schema.ts ./shared/
|
|
||||||
COPY ./drizzle.config.ts ./
|
|
||||||
|
|
||||||
# Create a directory for logs
|
|
||||||
RUN mkdir -p /app/logs
|
|
||||||
|
|
||||||
# Set default environment variables (these should be overridden at runtime)
|
|
||||||
ENV DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
|
|
||||||
ENV REDIS_URL=redis://redis:6379
|
|
||||||
ENV SESSION_SECRET=changeme
|
|
||||||
|
|
||||||
# Set rate limiting configuration
|
|
||||||
ENV RATE_LIMIT_WINDOW_MS=900000
|
|
||||||
ENV RATE_LIMIT_MAX_REQUESTS=100
|
|
||||||
|
|
||||||
# Set compression level (1-9, where 9 is maximum compression)
|
|
||||||
ENV COMPRESSION_LEVEL=6
|
|
||||||
|
|
||||||
# Set logging level (debug, info, warn, error)
|
|
||||||
ENV LOG_LEVEL=info
|
|
||||||
|
|
||||||
# Expose the port the app runs on
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Health check to ensure the application is running properly
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:5000/api/health || exit 1
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
CMD ["node", "dist/index.js"]
|
|
||||||
@@ -10,25 +10,11 @@ import { connectToLdap, searchLdap, getLdapAvailableAttributes } from "./ldap";
|
|||||||
import { IStorage } from "./storage";
|
import { IStorage } from "./storage";
|
||||||
import { InsertLdapQuery, LdapQuery, InsertLdapQueryVersion } from "@shared/schema";
|
import { InsertLdapQuery, LdapQuery, InsertLdapQueryVersion } from "@shared/schema";
|
||||||
|
|
||||||
// Use middleware to check permissions
|
import { requirePermission } from "./authorization";
|
||||||
|
|
||||||
|
// Use the proper auth middleware from authorization.ts with option to allow API tokens
|
||||||
function hasPermission(permission: string) {
|
function hasPermission(permission: string) {
|
||||||
return (req: any, res: any, next: any) => {
|
return requirePermission(permission, { allowApiToken: true });
|
||||||
// Check if the user is authenticated via passport session or has a valid API token
|
|
||||||
if (
|
|
||||||
// Check for authenticated session (safely check if isAuthenticated is a function first)
|
|
||||||
(typeof req.isAuthenticated === 'function' && req.isAuthenticated()) ||
|
|
||||||
// Or check for existing user object (set by token auth)
|
|
||||||
req.user ||
|
|
||||||
// Or check for authorization header (token auth)
|
|
||||||
req.headers.authorization
|
|
||||||
) {
|
|
||||||
// In a real implementation, this would check the user's permissions against the required permission
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not authenticated, return 401 Unauthorized
|
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerLdapQueryBuilderRoutes(router: Router, storage: IStorage) {
|
export function registerLdapQueryBuilderRoutes(router: Router, storage: IStorage) {
|
||||||
|
|||||||
@@ -1,556 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import {
|
|
||||||
validateQueryBuilder,
|
|
||||||
generateLdapFilter,
|
|
||||||
getHumanReadableFilter,
|
|
||||||
buildLdapFilter,
|
|
||||||
LdapQueryBuilder
|
|
||||||
} from "./ldap-filter-builder";
|
|
||||||
import { connectToLdap, searchLdap } from "./ldap";
|
|
||||||
import { IStorage } from "./storage";
|
|
||||||
import { InsertLdapQuery, LdapQuery, InsertLdapQueryVersion } from "@shared/schema";
|
|
||||||
// Use middleware to check permissions
|
|
||||||
function hasPermission(permission: string) {
|
|
||||||
return (req: any, res: any, next: any) => {
|
|
||||||
// For now, we'll just allow all requests through
|
|
||||||
// In a real implementation, this would check the user's permissions
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerLdapQueryBuilderRoutes(router: Router, storage: IStorage) {
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries:
|
|
||||||
* get:
|
|
||||||
* summary: Get all saved LDAP queries
|
|
||||||
* description: Retrieve a list of all saved LDAP query filters
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: A list of LDAP queries
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const queries = await storage.getLdapQueries();
|
|
||||||
res.json(queries);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP queries:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP queries" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* get:
|
|
||||||
* summary: Get a saved LDAP query by ID
|
|
||||||
* description: Retrieve a specific LDAP query filter by its ID
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: LDAP query found
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries/:id", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = await storage.getLdapQueryById(id);
|
|
||||||
if (!query) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(query);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}/versions:
|
|
||||||
* get:
|
|
||||||
* summary: Get version history for an LDAP query
|
|
||||||
* description: Retrieve the revision history for a specific LDAP query
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: List of query versions
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/LdapQueryVersion'
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries/:id/versions", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = await storage.getLdapQueryById(id);
|
|
||||||
if (!query) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const versions = await storage.getLdapQueryVersions(id);
|
|
||||||
res.json(versions);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP query versions:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP query versions" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries:
|
|
||||||
* post:
|
|
||||||
* summary: Create a new LDAP query
|
|
||||||
* description: Save a new LDAP query filter
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: Query created successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query format
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { name, description, targetObject, filter } = req.body;
|
|
||||||
|
|
||||||
if (!name || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: name, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate the filter format
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
|
|
||||||
// Generate the LDAP filter and readable representation
|
|
||||||
const ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
const readableFilter = getHumanReadableFilter(queryBuilder.filter);
|
|
||||||
|
|
||||||
const insertQuery: InsertLdapQuery = {
|
|
||||||
name,
|
|
||||||
description: description || "",
|
|
||||||
targetObject,
|
|
||||||
filterJson: filter,
|
|
||||||
ldapFilter,
|
|
||||||
readableFilter,
|
|
||||||
createdBy: req.user?.id || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const query = await storage.createLdapQuery(insertQuery);
|
|
||||||
|
|
||||||
res.status(201).json(query);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error validating query:", error);
|
|
||||||
return res.status(400).json({ error: "Invalid query format", details: error.message });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to create LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* put:
|
|
||||||
* summary: Update an LDAP query
|
|
||||||
* description: Update an existing LDAP query and create a new version
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query updated successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query format
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.put("/ldap-queries/:id", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, description, targetObject, filter } = req.body;
|
|
||||||
|
|
||||||
if (!name || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: name, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQueryById(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate the filter format
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
|
|
||||||
// Generate the LDAP filter and readable representation
|
|
||||||
const ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
const readableFilter = getHumanReadableFilter(queryBuilder.filter);
|
|
||||||
|
|
||||||
// Create a version entry for the previous state
|
|
||||||
const versionEntry: InsertLdapQueryVersion = {
|
|
||||||
queryId: id,
|
|
||||||
version: existingQuery.version,
|
|
||||||
filterJson: existingQuery.filterJson as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter: existingQuery.ldapFilter,
|
|
||||||
readableFilter: existingQuery.readableFilter,
|
|
||||||
targetObject: existingQuery.targetObject,
|
|
||||||
createdBy: existingQuery.createdBy,
|
|
||||||
createdAt: existingQuery.createdAt
|
|
||||||
};
|
|
||||||
|
|
||||||
await storage.createLdapQueryVersion(versionEntry);
|
|
||||||
|
|
||||||
// Update the query with new values
|
|
||||||
const updatedQuery = await storage.updateLdapQuery(id, {
|
|
||||||
name,
|
|
||||||
description: description || "",
|
|
||||||
targetObject,
|
|
||||||
filterJson: filter,
|
|
||||||
ldapFilter,
|
|
||||||
readableFilter,
|
|
||||||
updatedBy: req.user?.id || null,
|
|
||||||
version: existingQuery.version + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(updatedQuery);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error validating query:", error);
|
|
||||||
return res.status(400).json({ error: "Invalid query format", details: error.message });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to update LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* delete:
|
|
||||||
* summary: Delete an LDAP query
|
|
||||||
* description: Delete an LDAP query and all its versions
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 204:
|
|
||||||
* description: Query deleted successfully
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.delete("/ldap-queries/:id", hasPermission("delete:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQueryById(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all versions and the query itself
|
|
||||||
await storage.deleteLdapQueryVersions(id);
|
|
||||||
await storage.deleteLdapQuery(id);
|
|
||||||
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to delete LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}/revert/{version}:
|
|
||||||
* post:
|
|
||||||
* summary: Revert to a previous version
|
|
||||||
* description: Revert an LDAP query to a specific previous version
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* - in: path
|
|
||||||
* name: version
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Version to revert to
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query reverted successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 404:
|
|
||||||
* description: Query or version not found
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries/:id/revert/:version", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
const version = parseInt(req.params.version);
|
|
||||||
|
|
||||||
if (isNaN(id) || isNaN(version)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID or version" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQueryById(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the version to revert to
|
|
||||||
const versionToRevert = await storage.getLdapQueryVersion(id, version);
|
|
||||||
if (!versionToRevert) {
|
|
||||||
return res.status(404).json({ error: "Version not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a version entry for the current state
|
|
||||||
const versionEntry: InsertLdapQueryVersion = {
|
|
||||||
queryId: id,
|
|
||||||
version: existingQuery.version,
|
|
||||||
filterJson: existingQuery.filterJson as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter: existingQuery.ldapFilter,
|
|
||||||
readableFilter: existingQuery.readableFilter,
|
|
||||||
targetObject: existingQuery.targetObject,
|
|
||||||
createdBy: existingQuery.createdBy,
|
|
||||||
createdAt: existingQuery.createdAt
|
|
||||||
};
|
|
||||||
|
|
||||||
await storage.createLdapQueryVersion(versionEntry);
|
|
||||||
|
|
||||||
// Update the query with the version's values
|
|
||||||
const updatedQuery = await storage.updateLdapQuery(id, {
|
|
||||||
targetObject: versionToRevert.targetObject,
|
|
||||||
filterJson: versionToRevert.filterJson,
|
|
||||||
ldapFilter: versionToRevert.ldapFilter,
|
|
||||||
readableFilter: versionToRevert.readableFilter,
|
|
||||||
updatedBy: req.user?.id || null,
|
|
||||||
version: existingQuery.version + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(updatedQuery);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reverting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to revert LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/test:
|
|
||||||
* post:
|
|
||||||
* summary: Test an LDAP query filter
|
|
||||||
* description: Test a filter against an LDAP connection to see what objects would be returned
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* connectionId:
|
|
||||||
* type: integer
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* limit:
|
|
||||||
* type: integer
|
|
||||||
* default: 100
|
|
||||||
* properties:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query results
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* count:
|
|
||||||
* type: integer
|
|
||||||
* results:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: object
|
|
||||||
* filter:
|
|
||||||
* type: string
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query or connection
|
|
||||||
* 404:
|
|
||||||
* description: Connection not found
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries/test", hasPermission("read:ldap_objects"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { connectionId, targetObject, filter, limit = 100, properties = [] } = req.body;
|
|
||||||
|
|
||||||
if (!connectionId || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: connectionId, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the connection
|
|
||||||
const connection = await storage.getLdapConnectionById(connectionId);
|
|
||||||
if (!connection) {
|
|
||||||
return res.status(404).json({ error: "LDAP connection not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate and generate the filter
|
|
||||||
let ldapFilter: string;
|
|
||||||
try {
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({ error: "Invalid filter format", details: error.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to LDAP
|
|
||||||
const client = await connectToLdap(connection);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Execute the search
|
|
||||||
const results = await searchLdap(client, {
|
|
||||||
filter: ldapFilter,
|
|
||||||
attributes: properties.length > 0 ? properties : undefined,
|
|
||||||
limit
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the results
|
|
||||||
res.json({
|
|
||||||
count: results.length,
|
|
||||||
results,
|
|
||||||
filter: ldapFilter
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Always destroy the client
|
|
||||||
client.destroy();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error testing LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to test LDAP query", details: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,558 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import {
|
|
||||||
validateQueryBuilder,
|
|
||||||
generateLdapFilter,
|
|
||||||
getHumanReadableFilter,
|
|
||||||
buildLdapFilter,
|
|
||||||
LdapQueryBuilder
|
|
||||||
} from "./ldap-filter-builder";
|
|
||||||
import { connectToLdap, searchLdap } from "./ldap";
|
|
||||||
import { IStorage } from "./storage";
|
|
||||||
import { InsertLdapQuery, LdapQuery, InsertLdapQueryVersion } from "@shared/schema";
|
|
||||||
|
|
||||||
// Use middleware to check permissions
|
|
||||||
function hasPermission(permission: string) {
|
|
||||||
return (req: any, res: any, next: any) => {
|
|
||||||
// For now, we'll just allow all requests through
|
|
||||||
// In a real implementation, this would check the user's permissions
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerLdapQueryBuilderRoutes(router: Router, storage: IStorage) {
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries:
|
|
||||||
* get:
|
|
||||||
* summary: Get all saved LDAP queries
|
|
||||||
* description: Retrieve a list of all saved LDAP query filters
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: A list of LDAP queries
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const queries = await storage.getLdapQueries();
|
|
||||||
res.json(queries);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP queries:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP queries" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* get:
|
|
||||||
* summary: Get a saved LDAP query by ID
|
|
||||||
* description: Retrieve a specific LDAP query filter by its ID
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: LDAP query found
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries/:id", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = await storage.getLdapQuery(id);
|
|
||||||
if (!query) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(query);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}/versions:
|
|
||||||
* get:
|
|
||||||
* summary: Get version history for an LDAP query
|
|
||||||
* description: Retrieve the revision history for a specific LDAP query
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: List of query versions
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/LdapQueryVersion'
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries/:id/versions", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = await storage.getLdapQuery(id);
|
|
||||||
if (!query) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const versions = await storage.getLdapQueryVersions(id);
|
|
||||||
res.json(versions);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP query versions:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP query versions" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries:
|
|
||||||
* post:
|
|
||||||
* summary: Create a new LDAP query
|
|
||||||
* description: Save a new LDAP query filter
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: Query created successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query format
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { name, description, targetObject, filter } = req.body;
|
|
||||||
|
|
||||||
if (!name || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: name, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate the filter format
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
|
|
||||||
// Generate the LDAP filter and readable representation
|
|
||||||
const ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
const readableFilter = getHumanReadableFilter(queryBuilder.filter);
|
|
||||||
|
|
||||||
const insertQuery: InsertLdapQuery = {
|
|
||||||
name,
|
|
||||||
description: description || "",
|
|
||||||
targetObject,
|
|
||||||
filterJson: filter as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter,
|
|
||||||
readableFilter,
|
|
||||||
createdBy: req.user?.id || 1, // Default to 1 if no user
|
|
||||||
};
|
|
||||||
|
|
||||||
const query = await storage.createLdapQuery(insertQuery);
|
|
||||||
|
|
||||||
res.status(201).json(query);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error validating query:", error);
|
|
||||||
return res.status(400).json({ error: "Invalid query format", details: error.message });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to create LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* put:
|
|
||||||
* summary: Update an LDAP query
|
|
||||||
* description: Update an existing LDAP query and create a new version
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query updated successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query format
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.put("/ldap-queries/:id", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, description, targetObject, filter } = req.body;
|
|
||||||
|
|
||||||
if (!name || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: name, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQuery(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate the filter format
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
|
|
||||||
// Generate the LDAP filter and readable representation
|
|
||||||
const ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
const readableFilter = getHumanReadableFilter(queryBuilder.filter);
|
|
||||||
|
|
||||||
// Create a version entry for the previous state
|
|
||||||
const versionEntry: InsertLdapQueryVersion = {
|
|
||||||
queryId: id,
|
|
||||||
version: existingQuery.version,
|
|
||||||
filterJson: existingQuery.filterJson as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter: existingQuery.ldapFilter,
|
|
||||||
readableFilter: existingQuery.readableFilter,
|
|
||||||
targetObject: existingQuery.targetObject,
|
|
||||||
createdBy: existingQuery.createdBy,
|
|
||||||
createdAt: existingQuery.createdAt
|
|
||||||
};
|
|
||||||
|
|
||||||
await storage.createLdapQueryVersion(versionEntry);
|
|
||||||
|
|
||||||
// Update the query with new values
|
|
||||||
const updatedQuery = await storage.updateLdapQuery(id, {
|
|
||||||
name,
|
|
||||||
description: description || "",
|
|
||||||
targetObject,
|
|
||||||
filterJson: filter as any,
|
|
||||||
ldapFilter,
|
|
||||||
readableFilter,
|
|
||||||
updatedBy: req.user?.id || 1, // Default to 1 if no user
|
|
||||||
version: existingQuery.version + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(updatedQuery);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error validating query:", error);
|
|
||||||
return res.status(400).json({ error: "Invalid query format", details: error.message });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to update LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* delete:
|
|
||||||
* summary: Delete an LDAP query
|
|
||||||
* description: Delete an LDAP query and all its versions
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 204:
|
|
||||||
* description: Query deleted successfully
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.delete("/ldap-queries/:id", hasPermission("delete:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQuery(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the query - versions will be deleted via cascading foreign key constraints
|
|
||||||
await storage.deleteLdapQuery(id);
|
|
||||||
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to delete LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}/revert/{version}:
|
|
||||||
* post:
|
|
||||||
* summary: Revert to a previous version
|
|
||||||
* description: Revert an LDAP query to a specific previous version
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* - in: path
|
|
||||||
* name: version
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Version to revert to
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query reverted successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 404:
|
|
||||||
* description: Query or version not found
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries/:id/revert/:version", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
const versionNumber = parseInt(req.params.version);
|
|
||||||
|
|
||||||
if (isNaN(id) || isNaN(versionNumber)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID or version" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQuery(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the version to revert to - we need to get all versions and find the matching one
|
|
||||||
const allVersions = await storage.getLdapQueryVersions(id);
|
|
||||||
const versionToRevert = allVersions.find(v => v.version === versionNumber);
|
|
||||||
|
|
||||||
if (!versionToRevert) {
|
|
||||||
return res.status(404).json({ error: "Version not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a version entry for the current state
|
|
||||||
const versionEntry: InsertLdapQueryVersion = {
|
|
||||||
queryId: id,
|
|
||||||
version: existingQuery.version,
|
|
||||||
filterJson: existingQuery.filterJson as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter: existingQuery.ldapFilter,
|
|
||||||
readableFilter: existingQuery.readableFilter,
|
|
||||||
targetObject: existingQuery.targetObject,
|
|
||||||
createdBy: existingQuery.createdBy,
|
|
||||||
createdAt: existingQuery.createdAt
|
|
||||||
};
|
|
||||||
|
|
||||||
await storage.createLdapQueryVersion(versionEntry);
|
|
||||||
|
|
||||||
// Update the query with the version's values
|
|
||||||
const updatedQuery = await storage.updateLdapQuery(id, {
|
|
||||||
targetObject: versionToRevert.targetObject,
|
|
||||||
filterJson: versionToRevert.filterJson as any,
|
|
||||||
ldapFilter: versionToRevert.ldapFilter,
|
|
||||||
readableFilter: versionToRevert.readableFilter,
|
|
||||||
updatedBy: req.user?.id || 1, // Default to 1 if no user
|
|
||||||
version: existingQuery.version + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(updatedQuery);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reverting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to revert LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/test:
|
|
||||||
* post:
|
|
||||||
* summary: Test an LDAP query filter
|
|
||||||
* description: Test a filter against an LDAP connection to see what objects would be returned
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* connectionId:
|
|
||||||
* type: integer
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* limit:
|
|
||||||
* type: integer
|
|
||||||
* default: 100
|
|
||||||
* properties:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query results
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* count:
|
|
||||||
* type: integer
|
|
||||||
* results:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: object
|
|
||||||
* filter:
|
|
||||||
* type: string
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query or connection
|
|
||||||
* 404:
|
|
||||||
* description: Connection not found
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries/test", hasPermission("read:ldap_objects"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { connectionId, targetObject, filter, limit = 100, properties = [] } = req.body;
|
|
||||||
|
|
||||||
if (!connectionId || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: connectionId, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the connection
|
|
||||||
const connection = await storage.getLdapConnection(connectionId);
|
|
||||||
if (!connection) {
|
|
||||||
return res.status(404).json({ error: "LDAP connection not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate and generate the filter
|
|
||||||
let ldapFilter: string;
|
|
||||||
try {
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
} catch (error: any) {
|
|
||||||
return res.status(400).json({ error: "Invalid filter format", details: error.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to LDAP
|
|
||||||
const client = await connectToLdap(connection);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Execute the search
|
|
||||||
const results = await searchLdap(client, {
|
|
||||||
filter: ldapFilter,
|
|
||||||
attributes: properties.length > 0 ? properties : undefined,
|
|
||||||
limit
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the results
|
|
||||||
res.json({
|
|
||||||
count: results.length,
|
|
||||||
results,
|
|
||||||
filter: ldapFilter
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Always destroy the client
|
|
||||||
client.destroy();
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error testing LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to test LDAP query", details: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+1
-4
@@ -104,14 +104,11 @@ export interface IStorage {
|
|||||||
updateAdDomain(id: number, domain: Partial<AdDomain>): Promise<AdDomain | undefined>;
|
updateAdDomain(id: number, domain: Partial<AdDomain>): Promise<AdDomain | undefined>;
|
||||||
deleteAdDomain(id: number): Promise<boolean>;
|
deleteAdDomain(id: number): Promise<boolean>;
|
||||||
listAdDomains(connectionId: number, query?: any): Promise<AdDomain[]>;
|
listAdDomains(connectionId: number, query?: any): Promise<AdDomain[]>;
|
||||||
|
|
||||||
// Session store
|
|
||||||
sessionStore: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database Storage implementation
|
// Database Storage implementation
|
||||||
export class DatabaseStorage implements IStorage {
|
export class DatabaseStorage implements IStorage {
|
||||||
sessionStore: any;
|
sessionStore: session.Store;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sessionStore = new PostgresStore({
|
this.sessionStore = new PostgresStore({
|
||||||
|
|||||||
Reference in New Issue
Block a user