mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-27 19:17:07 +00:00
Add LDAP query builder feature with REST API and UI.
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/321aa6e4-ad8d-46b1-8b60-5a6d4c572512.jpg
This commit is contained in:
@@ -0,0 +1,213 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
+31
-5
@@ -11,12 +11,17 @@ RUN npm ci
|
|||||||
# Copy the rest of the source code
|
# Copy the rest of the source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application
|
# Build the application (optimized for production)
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-slim AS runner
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
# Set environment to production
|
# Set environment to production
|
||||||
@@ -33,14 +38,35 @@ COPY --from=builder /app/dist ./dist
|
|||||||
COPY ./shared/schema.ts ./shared/
|
COPY ./shared/schema.ts ./shared/
|
||||||
COPY ./drizzle.config.ts ./
|
COPY ./drizzle.config.ts ./
|
||||||
|
|
||||||
# Set the database URL environment variable (this will be overridden at runtime)
|
# Create a directory for logs
|
||||||
ENV DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
# Set session secret (should be overridden at runtime)
|
# 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
|
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 the port the app runs on
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
# Start the application
|
# 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"]
|
CMD ["node", "dist/index.js"]
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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"]
|
||||||
Generated
+225
@@ -44,6 +44,7 @@
|
|||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"@types/ldapjs": "^3.0.6",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"ldapjs": "^3.0.7",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
@@ -1458,6 +1460,101 @@
|
|||||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@ldapjs/asn1": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/attribute": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"process-warning": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/change": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"@ldapjs/attribute": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/controls": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "^1.2.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/dn": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"process-warning": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/filter": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "2.0.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"process-warning": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/messages": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "^2.0.0",
|
||||||
|
"@ldapjs/attribute": "^1.0.0",
|
||||||
|
"@ldapjs/change": "^1.0.0",
|
||||||
|
"@ldapjs/controls": "^2.1.0",
|
||||||
|
"@ldapjs/dn": "^1.1.0",
|
||||||
|
"@ldapjs/filter": "^2.1.1",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"process-warning": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ldapjs/protocol": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@neondatabase/serverless": {
|
"node_modules/@neondatabase/serverless": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz",
|
||||||
@@ -3541,6 +3638,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ldapjs": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-E2Tn1ltJDYBsidOT9QG4engaQeQzRQ9aYNxVmjCkD33F7cIeLPgrRDXAYs0O35mK2YDU20c/+ZkNjeAPRGLM0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -3730,6 +3836,12 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0"
|
"vite": "^4.2.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/abstract-logging": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -3816,6 +3928,15 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assert-plus": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.20",
|
"version": "10.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||||
@@ -3854,6 +3975,18 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/backoff": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"precond": "0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -4313,6 +4446,12 @@
|
|||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -5490,6 +5629,15 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/extsprintf": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
|
||||||
|
"engines": [
|
||||||
|
"node >=0.6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-equals": {
|
"node_modules/fast-equals": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
||||||
@@ -6148,6 +6296,29 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ldapjs": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==",
|
||||||
|
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ldapjs/asn1": "^2.0.0",
|
||||||
|
"@ldapjs/attribute": "^1.0.0",
|
||||||
|
"@ldapjs/change": "^1.0.0",
|
||||||
|
"@ldapjs/controls": "^2.1.0",
|
||||||
|
"@ldapjs/dn": "^1.1.0",
|
||||||
|
"@ldapjs/filter": "^2.1.1",
|
||||||
|
"@ldapjs/messages": "^1.3.0",
|
||||||
|
"@ldapjs/protocol": "^1.2.1",
|
||||||
|
"abstract-logging": "^2.0.1",
|
||||||
|
"assert-plus": "^1.0.0",
|
||||||
|
"backoff": "^2.5.0",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"vasync": "^2.2.1",
|
||||||
|
"verror": "^1.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||||
@@ -7161,6 +7332,20 @@
|
|||||||
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/precond": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-warning": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -8897,6 +9082,32 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vasync": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
|
||||||
|
"engines": [
|
||||||
|
"node >=0.6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"verror": "1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vasync/node_modules/verror": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||||
|
"engines": [
|
||||||
|
"node >=0.6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": "^1.0.0",
|
||||||
|
"core-util-is": "1.0.2",
|
||||||
|
"extsprintf": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz",
|
||||||
@@ -8910,6 +9121,20 @@
|
|||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/verror": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": "^1.0.0",
|
||||||
|
"core-util-is": "1.0.2",
|
||||||
|
"extsprintf": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "36.9.2",
|
"version": "36.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"@types/ldapjs": "^3.0.6",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"ldapjs": "^3.0.7",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Define the operator types for LDAP filters
|
||||||
|
export enum LdapOperator {
|
||||||
|
// Logical operators
|
||||||
|
AND = "and",
|
||||||
|
OR = "or",
|
||||||
|
NOT = "not",
|
||||||
|
|
||||||
|
// Comparison operators
|
||||||
|
EQUALS = "equals",
|
||||||
|
NOT_EQUALS = "notEquals",
|
||||||
|
STARTS_WITH = "startsWith",
|
||||||
|
ENDS_WITH = "endsWith",
|
||||||
|
CONTAINS = "contains",
|
||||||
|
GREATER_THAN = "greaterThan",
|
||||||
|
LESS_THAN = "lessThan",
|
||||||
|
PRESENT = "present", // attribute exists
|
||||||
|
APPROX = "approx", // approximately equals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the condition schema for LDAP filter conditions
|
||||||
|
export type LdapCondition = {
|
||||||
|
operator: LdapOperator;
|
||||||
|
attribute?: string; // Not required for logical operators (AND, OR, NOT)
|
||||||
|
value?: string; // Not required for some operators (PRESENT, logical operators)
|
||||||
|
conditions?: LdapCondition[]; // For nested conditions with logical operators
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ldapConditionSchema: z.ZodType<LdapCondition> = z.object({
|
||||||
|
operator: z.nativeEnum(LdapOperator),
|
||||||
|
attribute: z.string().optional(), // Not required for logical operators (AND, OR, NOT)
|
||||||
|
value: z.string().optional(), // Not required for some operators (PRESENT, logical operators)
|
||||||
|
conditions: z.array(z.lazy(() => ldapConditionSchema)).optional(), // For nested conditions with logical operators
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the query builder schema
|
||||||
|
export const ldapQueryBuilderSchema = z.object({
|
||||||
|
targetObject: z.enum(["users", "groups", "computers", "ous"]),
|
||||||
|
filter: ldapConditionSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type definitions for the query builder
|
||||||
|
export type LdapQueryBuilder = z.infer<typeof ldapQueryBuilderSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a condition object to a valid LDAP filter string
|
||||||
|
*/
|
||||||
|
export function buildLdapFilter(condition: LdapCondition): string {
|
||||||
|
// Handle logical operators
|
||||||
|
if (condition.operator === LdapOperator.AND && condition.conditions) {
|
||||||
|
const subConditions = condition.conditions.map(buildLdapFilter).join("");
|
||||||
|
return `(&${subConditions})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition.operator === LdapOperator.OR && condition.conditions) {
|
||||||
|
const subConditions = condition.conditions.map(buildLdapFilter).join("");
|
||||||
|
return `(|${subConditions})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition.operator === LdapOperator.NOT && condition.conditions && condition.conditions.length > 0) {
|
||||||
|
return `(!${buildLdapFilter(condition.conditions[0])})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle comparison operators
|
||||||
|
if (!condition.attribute) {
|
||||||
|
throw new Error(`Attribute is required for operator ${condition.operator}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (condition.operator) {
|
||||||
|
case LdapOperator.EQUALS:
|
||||||
|
return `(${condition.attribute}=${escapeFilterValue(condition.value || "")})`;
|
||||||
|
case LdapOperator.NOT_EQUALS:
|
||||||
|
return `(!(${condition.attribute}=${escapeFilterValue(condition.value || "")}))`;
|
||||||
|
case LdapOperator.STARTS_WITH:
|
||||||
|
return `(${condition.attribute}=${escapeFilterValue(condition.value || "")}*)`;
|
||||||
|
case LdapOperator.ENDS_WITH:
|
||||||
|
return `(${condition.attribute}=*${escapeFilterValue(condition.value || "")})`;
|
||||||
|
case LdapOperator.CONTAINS:
|
||||||
|
return `(${condition.attribute}=*${escapeFilterValue(condition.value || "")}*)`;
|
||||||
|
case LdapOperator.GREATER_THAN:
|
||||||
|
return `(${condition.attribute}>${escapeFilterValue(condition.value || "")})`;
|
||||||
|
case LdapOperator.LESS_THAN:
|
||||||
|
return `(${condition.attribute}<${escapeFilterValue(condition.value || "")})`;
|
||||||
|
case LdapOperator.PRESENT:
|
||||||
|
return `(${condition.attribute}=*)`;
|
||||||
|
case LdapOperator.APPROX:
|
||||||
|
return `(${condition.attribute}~=${escapeFilterValue(condition.value || "")})`;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported operator: ${condition.operator}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get common LDAP object classes for different target object types
|
||||||
|
*/
|
||||||
|
export function getObjectClassFilter(targetObject: string): string {
|
||||||
|
switch (targetObject) {
|
||||||
|
case "users":
|
||||||
|
return "(&(objectClass=user)(!(objectClass=computer)))";
|
||||||
|
case "groups":
|
||||||
|
return "(objectClass=group)";
|
||||||
|
case "computers":
|
||||||
|
return "(objectClass=computer)";
|
||||||
|
case "ous":
|
||||||
|
return "(objectClass=organizationalUnit)";
|
||||||
|
default:
|
||||||
|
return "(objectClass=*)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a combined LDAP filter by joining the user-defined filter with the appropriate object class filter
|
||||||
|
*/
|
||||||
|
export function generateLdapFilter(queryBuilder: LdapQueryBuilder): string {
|
||||||
|
const objectClassFilter = getObjectClassFilter(queryBuilder.targetObject);
|
||||||
|
const userFilter = buildLdapFilter(queryBuilder.filter);
|
||||||
|
|
||||||
|
// Combine the two filters with AND
|
||||||
|
return `(&${objectClassFilter}${userFilter})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape special characters in LDAP filter values according to RFC 4515
|
||||||
|
*/
|
||||||
|
function escapeFilterValue(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/\\/g, "\\5c") // Must be first to avoid double escaping
|
||||||
|
.replace(/\*/g, "\\2a")
|
||||||
|
.replace(/\(/g, "\\28")
|
||||||
|
.replace(/\)/g, "\\29")
|
||||||
|
.replace(/\0/g, "\\00")
|
||||||
|
.replace(/\//g, "\\2f");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a query builder object and ensure it has the required properties
|
||||||
|
*/
|
||||||
|
export function validateQueryBuilder(queryBuilder: unknown): LdapQueryBuilder {
|
||||||
|
return ldapQueryBuilderSchema.parse(queryBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable text representation of an LDAP filter condition
|
||||||
|
*/
|
||||||
|
export function getHumanReadableFilter(condition: LdapCondition, indent = 0): string {
|
||||||
|
const spaces = " ".repeat(indent);
|
||||||
|
|
||||||
|
switch (condition.operator) {
|
||||||
|
case LdapOperator.AND:
|
||||||
|
return `${spaces}ALL of the following conditions:\n` +
|
||||||
|
(condition.conditions?.map(c => getHumanReadableFilter(c, indent + 2)).join("\n") || "");
|
||||||
|
|
||||||
|
case LdapOperator.OR:
|
||||||
|
return `${spaces}ANY of the following conditions:\n` +
|
||||||
|
(condition.conditions?.map(c => getHumanReadableFilter(c, indent + 2)).join("\n") || "");
|
||||||
|
|
||||||
|
case LdapOperator.NOT:
|
||||||
|
return `${spaces}NOT the following condition:\n` +
|
||||||
|
(condition.conditions && condition.conditions.length > 0
|
||||||
|
? getHumanReadableFilter(condition.conditions[0], indent + 2)
|
||||||
|
: "");
|
||||||
|
|
||||||
|
case LdapOperator.EQUALS:
|
||||||
|
return `${spaces}${condition.attribute} equals "${condition.value}"`;
|
||||||
|
|
||||||
|
case LdapOperator.NOT_EQUALS:
|
||||||
|
return `${spaces}${condition.attribute} does not equal "${condition.value}"`;
|
||||||
|
|
||||||
|
case LdapOperator.STARTS_WITH:
|
||||||
|
return `${spaces}${condition.attribute} starts with "${condition.value}"`;
|
||||||
|
|
||||||
|
case LdapOperator.ENDS_WITH:
|
||||||
|
return `${spaces}${condition.attribute} ends with "${condition.value}"`;
|
||||||
|
|
||||||
|
case LdapOperator.CONTAINS:
|
||||||
|
return `${spaces}${condition.attribute} contains "${condition.value}"`;
|
||||||
|
|
||||||
|
case LdapOperator.GREATER_THAN:
|
||||||
|
return `${spaces}${condition.attribute} is greater than "${condition.value}"`;
|
||||||
|
|
||||||
|
case LdapOperator.LESS_THAN:
|
||||||
|
return `${spaces}${condition.attribute} is less than "${condition.value}"`;
|
||||||
|
|
||||||
|
case LdapOperator.PRESENT:
|
||||||
|
return `${spaces}${condition.attribute} exists`;
|
||||||
|
|
||||||
|
case LdapOperator.APPROX:
|
||||||
|
return `${spaces}${condition.attribute} is approximately equal to "${condition.value}"`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return `${spaces}Unknown condition`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
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,
|
||||||
|
modifiedBy: req.user?.id || 1 // Default to 1 if no user
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
modifiedBy: 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,
|
||||||
|
modifiedBy: req.user?.id || 1 // Default to 1 if no user
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
modifiedBy: 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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,556 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
+123
-193
@@ -1,201 +1,131 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { LdapConnection } from "@shared/schema";
|
||||||
import ldap from 'ldapjs';
|
import debugLib from "debug";
|
||||||
import { LdapConnection } from '@shared/schema';
|
import * as ldapjs from "ldapjs";
|
||||||
import { storage } from './storage';
|
import { promisify } from "util";
|
||||||
|
|
||||||
class LdapClient extends EventEmitter {
|
const debug = debugLib("app:ldap");
|
||||||
private clients: Map<number, ldap.Client> = new Map();
|
|
||||||
private isConnected: Map<number, boolean> = new Map();
|
/**
|
||||||
|
* Connect to LDAP server and return a client
|
||||||
|
*/
|
||||||
|
export async function connectToLdap(connection: LdapConnection): Promise<ldapjs.Client> {
|
||||||
|
const url = `${connection.useSSL ? "ldaps" : "ldap"}://${connection.server}:${connection.port}`;
|
||||||
|
|
||||||
async connect(connection: LdapConnection): Promise<boolean> {
|
debug(`Connecting to LDAP server at ${url}`);
|
||||||
try {
|
|
||||||
const clientOptions: ldap.ClientOptions = {
|
const client = ldapjs.createClient({
|
||||||
url: `${connection.useTLS ? 'ldaps' : 'ldap'}://${connection.server}:${connection.port}`,
|
url,
|
||||||
reconnect: {
|
timeout: 5000,
|
||||||
initialDelay: 1000,
|
connectTimeout: 10000,
|
||||||
maxDelay: 10000,
|
idleTimeout: 30000,
|
||||||
failAfter: 10
|
reconnect: {
|
||||||
},
|
initialDelay: 100,
|
||||||
timeout: 5000,
|
maxDelay: 1000,
|
||||||
connectTimeout: 10000
|
failAfter: 10
|
||||||
};
|
|
||||||
|
|
||||||
const client = ldap.createClient(clientOptions);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
client.on('error', async (err) => {
|
|
||||||
console.error(`LDAP connection error for ${connection.name}:`, err);
|
|
||||||
this.isConnected.set(connection.id, false);
|
|
||||||
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
|
|
||||||
this.emit('status', {
|
|
||||||
connectionId: connection.id,
|
|
||||||
status: 'disconnected',
|
|
||||||
error: err.message
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.bind(connection.username, connection.password, async (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(`LDAP bind error for ${connection.name}:`, err);
|
|
||||||
this.isConnected.set(connection.id, false);
|
|
||||||
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
|
|
||||||
this.emit('status', {
|
|
||||||
connectionId: connection.id,
|
|
||||||
status: 'disconnected',
|
|
||||||
error: err.message
|
|
||||||
});
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clients.set(connection.id, client);
|
|
||||||
this.isConnected.set(connection.id, true);
|
|
||||||
await storage.updateLdapConnection(connection.id, {
|
|
||||||
status: 'connected',
|
|
||||||
lastConnected: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emit('status', {
|
|
||||||
connectionId: connection.id,
|
|
||||||
status: 'connected'
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`LDAP connection error for ${connection.name}:`, error);
|
|
||||||
this.isConnected.set(connection.id, false);
|
|
||||||
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
|
|
||||||
this.emit('status', {
|
|
||||||
connectionId: connection.id,
|
|
||||||
status: 'disconnected',
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
async disconnect(connectionId: number): Promise<void> {
|
// Convert bind to promise
|
||||||
const client = this.clients.get(connectionId);
|
const bindAsync = promisify(client.bind).bind(client);
|
||||||
if (client) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
client.unbind(() => {
|
|
||||||
this.clients.delete(connectionId);
|
|
||||||
this.isConnected.set(connectionId, false);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getClient(connectionId: number): ldap.Client | undefined {
|
try {
|
||||||
return this.clients.get(connectionId);
|
// Bind with credentials
|
||||||
}
|
await bindAsync(connection.username, connection.password);
|
||||||
|
debug("Successfully authenticated to LDAP server");
|
||||||
isConnectionActive(connectionId: number): boolean {
|
return client;
|
||||||
return this.isConnected.get(connectionId) || false;
|
} catch (error: unknown) {
|
||||||
}
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
debug("Failed to connect to LDAP server:", error);
|
||||||
// LDAP CRUD operations
|
throw new Error(`Failed to connect to LDAP server: ${errorMessage}`);
|
||||||
async searchUsers(connectionId: number, filter = '(objectClass=user)', attributes?: string[]): Promise<any[]> {
|
|
||||||
const client = this.getClient(connectionId);
|
|
||||||
if (!client) throw new Error('LDAP connection not established');
|
|
||||||
|
|
||||||
const connection = await storage.getLdapConnection(connectionId);
|
|
||||||
if (!connection) throw new Error('LDAP connection not found');
|
|
||||||
|
|
||||||
const baseDN = connection.baseDN || '';
|
|
||||||
const defaultAttributes = ['cn', 'sAMAccountName', 'mail', 'distinguishedName'];
|
|
||||||
const searchAttributes = attributes?.length ? attributes : defaultAttributes;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const results: any[] = [];
|
|
||||||
|
|
||||||
client.search(baseDN, {
|
|
||||||
filter,
|
|
||||||
scope: 'sub',
|
|
||||||
attributes: searchAttributes
|
|
||||||
}, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.on('searchEntry', (entry) => {
|
|
||||||
results.push(entry.object);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('error', (err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', (result) => {
|
|
||||||
resolve(results);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchGroups(connectionId: number, filter = '(objectClass=group)', attributes?: string[]): Promise<any[]> {
|
|
||||||
const defaultAttributes = ['cn', 'distinguishedName', 'member'];
|
|
||||||
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchOUs(connectionId: number, filter = '(objectClass=organizationalUnit)', attributes?: string[]): Promise<any[]> {
|
|
||||||
const defaultAttributes = ['ou', 'distinguishedName'];
|
|
||||||
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchComputers(connectionId: number, filter = '(objectClass=computer)', attributes?: string[]): Promise<any[]> {
|
|
||||||
const defaultAttributes = ['cn', 'distinguishedName', 'operatingSystem'];
|
|
||||||
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createEntry(connectionId: number, dn: string, attributes: any): Promise<boolean> {
|
|
||||||
const client = this.getClient(connectionId);
|
|
||||||
if (!client) throw new Error('LDAP connection not established');
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
client.add(dn, attributes, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateEntry(connectionId: number, dn: string, changes: any[]): Promise<boolean> {
|
|
||||||
const client = this.getClient(connectionId);
|
|
||||||
if (!client) throw new Error('LDAP connection not established');
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
client.modify(dn, changes, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteEntry(connectionId: number, dn: string): Promise<boolean> {
|
|
||||||
const client = this.getClient(connectionId);
|
|
||||||
if (!client) throw new Error('LDAP connection not established');
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
client.del(dn, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ldapClient = new LdapClient();
|
export interface LdapSearchOptions {
|
||||||
|
base?: string;
|
||||||
|
filter: string;
|
||||||
|
scope?: "base" | "one" | "sub";
|
||||||
|
attributes?: string[];
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search LDAP directory with the provided options
|
||||||
|
*/
|
||||||
|
export async function searchLdap(client: ldapjs.Client, options: LdapSearchOptions): Promise<Record<string, any>[]> {
|
||||||
|
const {
|
||||||
|
base = "",
|
||||||
|
filter,
|
||||||
|
scope = "sub",
|
||||||
|
attributes,
|
||||||
|
limit = 1000
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
debug(`Searching LDAP with filter: ${filter}`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const results: Record<string, any>[] = [];
|
||||||
|
|
||||||
|
client.search(base, {
|
||||||
|
filter,
|
||||||
|
scope, // ldapjs accepts 'base', 'one', 'sub' as strings
|
||||||
|
attributes,
|
||||||
|
sizeLimit: limit
|
||||||
|
}, (err: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse) => {
|
||||||
|
if (err) {
|
||||||
|
debug("LDAP search error:", err);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The types for ldapjs don't fully match the actual API
|
||||||
|
// We need to use any here because the type definitions are incomplete
|
||||||
|
res.on("searchEntry", (entry: any) => {
|
||||||
|
results.push(entry.object);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on("error", (err: ldapjs.Error) => {
|
||||||
|
debug("LDAP search result error:", err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on("end", (result: any) => {
|
||||||
|
debug(`LDAP search completed with ${results.length} results`);
|
||||||
|
if (result && result.status !== 0) {
|
||||||
|
debug(`LDAP search ended with status: ${result.status}`);
|
||||||
|
}
|
||||||
|
resolve(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a connection to an LDAP server
|
||||||
|
*/
|
||||||
|
export async function testLdapConnection(connection: LdapConnection): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const client = await connectToLdap(connection);
|
||||||
|
client.destroy();
|
||||||
|
return true;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
debug("LDAP connection test failed:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get basic info about an LDAP domain
|
||||||
|
*/
|
||||||
|
export async function getLdapDomainInfo(client: ldapjs.Client): Promise<Record<string, any> | null> {
|
||||||
|
try {
|
||||||
|
const results = await searchLdap(client, {
|
||||||
|
filter: "(objectClass=domain)",
|
||||||
|
scope: "base"
|
||||||
|
});
|
||||||
|
|
||||||
|
return results.length > 0 ? results[0] : null;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
debug("Failed to get LDAP domain info:", error);
|
||||||
|
throw new Error(`Failed to get LDAP domain info: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
requireAdmin,
|
requireAdmin,
|
||||||
initializeRBAC
|
initializeRBAC
|
||||||
} from "./authorization";
|
} from "./authorization";
|
||||||
|
import { registerLdapQueryBuilderRoutes } from "./ldap-query-builder-routes";
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
export async function registerRoutes(app: Express): Promise<Server> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Setup authentication
|
// Setup authentication
|
||||||
@@ -826,6 +828,11 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up LDAP query builder routes
|
||||||
|
const ldapQueryRouter = express.Router();
|
||||||
|
registerLdapQueryBuilderRoutes(ldapQueryRouter, storage);
|
||||||
|
app.use('/api', ldapQueryRouter);
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
|
|
||||||
return httpServer;
|
return httpServer;
|
||||||
|
|||||||
+147
-1
@@ -4,8 +4,9 @@ import {
|
|||||||
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
|
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
|
||||||
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
|
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
|
||||||
AdDomain, InsertAdDomain, Role, ApiQuery,
|
AdDomain, InsertAdDomain, Role, ApiQuery,
|
||||||
|
LdapQuery, InsertLdapQuery, LdapQueryVersion, InsertLdapQueryVersion,
|
||||||
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
|
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
|
||||||
roles
|
roles, ldapQueries, ldapQueryVersions
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
import createMemoryStore from "memorystore";
|
import createMemoryStore from "memorystore";
|
||||||
@@ -57,6 +58,15 @@ export interface IStorage {
|
|||||||
deleteLdapConnection(id: number): Promise<boolean>;
|
deleteLdapConnection(id: number): Promise<boolean>;
|
||||||
listLdapConnections(): Promise<LdapConnection[]>;
|
listLdapConnections(): Promise<LdapConnection[]>;
|
||||||
|
|
||||||
|
// LDAP Query Builder
|
||||||
|
getLdapQuery(id: number): Promise<LdapQuery | undefined>;
|
||||||
|
getLdapQueries(): Promise<LdapQuery[]>;
|
||||||
|
createLdapQuery(query: InsertLdapQuery): Promise<LdapQuery>;
|
||||||
|
updateLdapQuery(id: number, query: Partial<LdapQuery>): Promise<LdapQuery | undefined>;
|
||||||
|
deleteLdapQuery(id: number): Promise<boolean>;
|
||||||
|
getLdapQueryVersions(queryId: number): Promise<LdapQueryVersion[]>;
|
||||||
|
createLdapQueryVersion(version: InsertLdapQueryVersion): Promise<LdapQueryVersion>;
|
||||||
|
|
||||||
// AD Users
|
// AD Users
|
||||||
getAdUser(id: number): Promise<AdUser | undefined>;
|
getAdUser(id: number): Promise<AdUser | undefined>;
|
||||||
createAdUser(user: InsertAdUser): Promise<AdUser>;
|
createAdUser(user: InsertAdUser): Promise<AdUser>;
|
||||||
@@ -198,6 +208,142 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return db.select().from(ldapConnections);
|
return db.select().from(ldapConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LDAP Query Builder
|
||||||
|
async getLdapQuery(id: number): Promise<LdapQuery | undefined> {
|
||||||
|
const cacheKey = `ldapQuery:${id}`;
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
const cachedData = await getCached<LdapQuery>(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
debug(`Cache hit for ${cacheKey}`);
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.select().from(ldapQueries).where(eq(ldapQueries.id, id));
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
// Cache the query for faster access
|
||||||
|
await setCached(cacheKey, result[0], CACHE_TTL.MEDIUM);
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLdapQueries(): Promise<LdapQuery[]> {
|
||||||
|
const cacheKey = 'ldapQueries:all';
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
const cachedData = await getCached<LdapQuery[]>(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
debug(`Cache hit for ${cacheKey}`);
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queries = await db.select().from(ldapQueries);
|
||||||
|
|
||||||
|
// Cache the results
|
||||||
|
await setCached(cacheKey, queries, CACHE_TTL.MEDIUM);
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLdapQuery(query: InsertLdapQuery): Promise<LdapQuery> {
|
||||||
|
// Create the query
|
||||||
|
const result = await db.insert(ldapQueries).values({
|
||||||
|
name: query.name,
|
||||||
|
description: query.description,
|
||||||
|
targetObject: query.targetObject,
|
||||||
|
filterJson: query.filterJson,
|
||||||
|
ldapFilter: query.ldapFilter,
|
||||||
|
readableFilter: query.readableFilter,
|
||||||
|
createdBy: query.createdBy,
|
||||||
|
modifiedBy: query.createdBy // Initially, creator and modifier are the same
|
||||||
|
}).returning();
|
||||||
|
|
||||||
|
// Invalidate relevant caches
|
||||||
|
invalidateCache('ldapQueries:all');
|
||||||
|
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLdapQuery(id: number, queryData: Partial<LdapQuery>): Promise<LdapQuery | undefined> {
|
||||||
|
// Update the query
|
||||||
|
const result = await db.update(ldapQueries)
|
||||||
|
.set({
|
||||||
|
...queryData,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(ldapQueries.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
// Invalidate relevant caches
|
||||||
|
invalidateCache(`ldapQuery:${id}`);
|
||||||
|
invalidateCache('ldapQueries:all');
|
||||||
|
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteLdapQuery(id: number): Promise<boolean> {
|
||||||
|
// Delete the query (versions will be deleted via cascade)
|
||||||
|
const result = await db.delete(ldapQueries)
|
||||||
|
.where(eq(ldapQueries.id, id))
|
||||||
|
.returning({ id: ldapQueries.id });
|
||||||
|
|
||||||
|
const deleted = result.length > 0;
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
// Invalidate relevant caches
|
||||||
|
invalidateCache(`ldapQuery:${id}`);
|
||||||
|
invalidateCachePattern(`ldapQueryVersions:${id}:*`);
|
||||||
|
invalidateCache('ldapQueries:all');
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLdapQueryVersions(queryId: number): Promise<LdapQueryVersion[]> {
|
||||||
|
const cacheKey = `ldapQueryVersions:${queryId}:all`;
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
const cachedData = await getCached<LdapQueryVersion[]>(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
debug(`Cache hit for ${cacheKey}`);
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = await db.select()
|
||||||
|
.from(ldapQueryVersions)
|
||||||
|
.where(eq(ldapQueryVersions.queryId, queryId))
|
||||||
|
.orderBy(ldapQueryVersions.version);
|
||||||
|
|
||||||
|
// Cache the results
|
||||||
|
await setCached(cacheKey, versions, CACHE_TTL.MEDIUM);
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLdapQueryVersion(version: InsertLdapQueryVersion): Promise<LdapQueryVersion> {
|
||||||
|
const result = await db.insert(ldapQueryVersions).values({
|
||||||
|
queryId: version.queryId,
|
||||||
|
version: version.version,
|
||||||
|
filterJson: version.filterJson,
|
||||||
|
ldapFilter: version.ldapFilter,
|
||||||
|
readableFilter: version.readableFilter,
|
||||||
|
targetObject: version.targetObject,
|
||||||
|
createdBy: version.createdBy,
|
||||||
|
modifiedBy: version.modifiedBy
|
||||||
|
}).returning();
|
||||||
|
|
||||||
|
// Invalidate relevant caches
|
||||||
|
invalidateCachePattern(`ldapQueryVersions:${version.queryId}:*`);
|
||||||
|
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
// AD Users
|
// AD Users
|
||||||
async getAdUser(id: number): Promise<AdUser | undefined> {
|
async getAdUser(id: number): Promise<AdUser | undefined> {
|
||||||
const result = await db.select().from(adUsers).where(eq(adUsers.id, id));
|
const result = await db.select().from(adUsers).where(eq(adUsers.id, id));
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ export const PERMISSIONS = {
|
|||||||
DELETE_AD_COMPUTERS: "delete:ad_computers",
|
DELETE_AD_COMPUTERS: "delete:ad_computers",
|
||||||
|
|
||||||
VIEW_AD_DOMAINS: "view:ad_domains",
|
VIEW_AD_DOMAINS: "view:ad_domains",
|
||||||
|
|
||||||
|
// LDAP Query Builder
|
||||||
|
VIEW_LDAP_QUERIES: "view:ldap_queries",
|
||||||
|
CREATE_LDAP_QUERIES: "create:ldap_queries",
|
||||||
|
UPDATE_LDAP_QUERIES: "update:ldap_queries",
|
||||||
|
DELETE_LDAP_QUERIES: "delete:ldap_queries",
|
||||||
|
RUN_LDAP_QUERIES: "run:ldap_queries",
|
||||||
|
|
||||||
// API Token management
|
// API Token management
|
||||||
MANAGE_API_TOKENS: "manage:api_tokens",
|
MANAGE_API_TOKENS: "manage:api_tokens",
|
||||||
@@ -84,6 +91,11 @@ export const permissionsSchema = z.enum([
|
|||||||
"update:ad_computers",
|
"update:ad_computers",
|
||||||
"delete:ad_computers",
|
"delete:ad_computers",
|
||||||
"view:ad_domains",
|
"view:ad_domains",
|
||||||
|
"view:ldap_queries",
|
||||||
|
"create:ldap_queries",
|
||||||
|
"update:ldap_queries",
|
||||||
|
"delete:ldap_queries",
|
||||||
|
"run:ldap_queries",
|
||||||
"manage:api_tokens",
|
"manage:api_tokens",
|
||||||
"manage:roles",
|
"manage:roles",
|
||||||
"admin:system"
|
"admin:system"
|
||||||
@@ -199,6 +211,36 @@ export const adDomains = pgTable("ad_domains", {
|
|||||||
adProperties: jsonb("ad_properties"),
|
adProperties: jsonb("ad_properties"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// LDAP Query Builder schemas
|
||||||
|
export const ldapQueries = pgTable("ldap_queries", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
targetObject: text("target_object").notNull(), // users, groups, computers, ous
|
||||||
|
filterJson: jsonb("filter").notNull(), // Serialized filter conditions
|
||||||
|
ldapFilter: text("ldap_filter").notNull(), // The actual LDAP filter string
|
||||||
|
readableFilter: text("readable_filter").notNull(), // Human-readable representation
|
||||||
|
version: integer("version").notNull().default(1),
|
||||||
|
createdBy: integer("created_by").notNull().references(() => users.id),
|
||||||
|
modifiedBy: integer("modified_by").notNull().references(() => users.id, { onDelete: "set null" }).default(1),
|
||||||
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// LDAP Query Versions for revision history
|
||||||
|
export const ldapQueryVersions = pgTable("ldap_query_versions", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
queryId: integer("query_id").notNull().references(() => ldapQueries.id, { onDelete: "cascade" }),
|
||||||
|
version: integer("version").notNull(),
|
||||||
|
filterJson: jsonb("filter").notNull(), // Serialized filter conditions
|
||||||
|
ldapFilter: text("ldap_filter").notNull(), // The actual LDAP filter string
|
||||||
|
readableFilter: text("readable_filter").notNull(), // Human-readable representation
|
||||||
|
targetObject: text("target_object").notNull(), // users, groups, computers, ous
|
||||||
|
createdBy: integer("created_by").notNull().references(() => users.id, { onDelete: "set null" }).default(1),
|
||||||
|
modifiedBy: integer("modified_by").notNull().references(() => users.id, { onDelete: "set null" }).default(1),
|
||||||
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
// Define relations between tables
|
// Define relations between tables
|
||||||
export const rolesRelations = relations(roles, ({ many }) => ({
|
export const rolesRelations = relations(roles, ({ many }) => ({
|
||||||
permissions: many(rolePermissions),
|
permissions: many(rolePermissions),
|
||||||
@@ -219,6 +261,8 @@ export const usersRelations = relations(users, ({ one, many }) => ({
|
|||||||
references: [roles.id],
|
references: [roles.id],
|
||||||
}),
|
}),
|
||||||
apiTokens: many(apiTokens),
|
apiTokens: many(apiTokens),
|
||||||
|
createdQueries: many(ldapQueries, { relationName: "createdQueries" }),
|
||||||
|
modifiedQueries: many(ldapQueries, { relationName: "modifiedQueries" }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
||||||
@@ -232,6 +276,31 @@ export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const ldapQueriesRelations = relations(ldapQueries, ({ one, many }) => ({
|
||||||
|
creator: one(users, {
|
||||||
|
fields: [ldapQueries.createdBy],
|
||||||
|
references: [users.id],
|
||||||
|
relationName: "createdQueries",
|
||||||
|
}),
|
||||||
|
modifier: one(users, {
|
||||||
|
fields: [ldapQueries.modifiedBy],
|
||||||
|
references: [users.id],
|
||||||
|
relationName: "modifiedQueries",
|
||||||
|
}),
|
||||||
|
versions: many(ldapQueryVersions),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ldapQueryVersionsRelations = relations(ldapQueryVersions, ({ one }) => ({
|
||||||
|
query: one(ldapQueries, {
|
||||||
|
fields: [ldapQueryVersions.queryId],
|
||||||
|
references: [ldapQueries.id],
|
||||||
|
}),
|
||||||
|
modifier: one(users, {
|
||||||
|
fields: [ldapQueryVersions.modifiedBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
// Generate insertion schemas
|
// Generate insertion schemas
|
||||||
export const insertRoleSchema = createInsertSchema(roles).omit({ id: true, createdAt: true });
|
export const insertRoleSchema = createInsertSchema(roles).omit({ id: true, createdAt: true });
|
||||||
export const insertRolePermissionSchema = createInsertSchema(rolePermissions);
|
export const insertRolePermissionSchema = createInsertSchema(rolePermissions);
|
||||||
@@ -243,6 +312,8 @@ export const insertAdGroupSchema = createInsertSchema(adGroups).omit({ id: true
|
|||||||
export const insertAdOrgUnitSchema = createInsertSchema(adOrgUnits).omit({ id: true });
|
export const insertAdOrgUnitSchema = createInsertSchema(adOrgUnits).omit({ id: true });
|
||||||
export const insertAdComputerSchema = createInsertSchema(adComputers).omit({ id: true });
|
export const insertAdComputerSchema = createInsertSchema(adComputers).omit({ id: true });
|
||||||
export const insertAdDomainSchema = createInsertSchema(adDomains).omit({ id: true });
|
export const insertAdDomainSchema = createInsertSchema(adDomains).omit({ id: true });
|
||||||
|
export const insertLdapQuerySchema = createInsertSchema(ldapQueries).omit({ id: true, createdAt: true, updatedAt: true, version: true });
|
||||||
|
export const insertLdapQueryVersionSchema = createInsertSchema(ldapQueryVersions).omit({ id: true, createdAt: true });
|
||||||
|
|
||||||
// Login schema
|
// Login schema
|
||||||
export const loginSchema = z.object({
|
export const loginSchema = z.object({
|
||||||
@@ -286,5 +357,9 @@ export type AdComputer = typeof adComputers.$inferSelect;
|
|||||||
export type InsertAdComputer = z.infer<typeof insertAdComputerSchema>;
|
export type InsertAdComputer = z.infer<typeof insertAdComputerSchema>;
|
||||||
export type AdDomain = typeof adDomains.$inferSelect;
|
export type AdDomain = typeof adDomains.$inferSelect;
|
||||||
export type InsertAdDomain = z.infer<typeof insertAdDomainSchema>;
|
export type InsertAdDomain = z.infer<typeof insertAdDomainSchema>;
|
||||||
|
export type LdapQuery = typeof ldapQueries.$inferSelect;
|
||||||
|
export type InsertLdapQuery = z.infer<typeof insertLdapQuerySchema>;
|
||||||
|
export type LdapQueryVersion = typeof ldapQueryVersions.$inferSelect;
|
||||||
|
export type InsertLdapQueryVersion = z.infer<typeof insertLdapQueryVersionSchema>;
|
||||||
export type Login = z.infer<typeof loginSchema>;
|
export type Login = z.infer<typeof loginSchema>;
|
||||||
export type ApiQuery = z.infer<typeof apiQuerySchema>;
|
export type ApiQuery = z.infer<typeof apiQuerySchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user