Implement IP Whitelisting
Priority
LOW - Business+ tier, security feature
Labels
enhancement, security, backend
Description
Allow organizations to restrict API and dashboard access to specific IP addresses or CIDR ranges. Essential for enterprise security policies.
Current State
- No IP-based access control
- Rate limiting uses IP for identification
- No IP whitelist/blacklist functionality
Implementation Overview
Database Schema
// backend/src/models/DRAIPWhitelist.ts
@Entity('dra_ip_whitelists')
export class DRAIPWhitelist {
@PrimaryGeneratedColumn()
id!: number;
@ManyToOne(() => DRAOrganization, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'organization_id' })
organization!: Relation<DRAOrganization>;
@Column({ type: 'varchar', length: 100 })
ip_address!: string; // Supports CIDR notation: 192.168.1.0/24
@Column({ type: 'varchar', length: 255, nullable: true })
description!: string | null; // e.g., "Office VPN"
@Column({ type: 'boolean', default: true })
is_active!: boolean;
@ManyToOne(() => DRAUsersPlatform)
@JoinColumn({ name: 'created_by_user_id' })
created_by!: Relation<DRAUsersPlatform>;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
created_at!: Date;
@Column({ type: 'timestamp', nullable: true })
last_used_at!: Date | null;
@Column({ type: 'int', default: 0 })
usage_count!: number;
}
IP Matching Service
// backend/src/services/IPWhitelistService.ts
import ipaddr from 'ipaddr.js';
export class IPWhitelistService {
private static instance: IPWhitelistService;
public static getInstance(): IPWhitelistService {
if (!IPWhitelistService.instance) {
IPWhitelistService.instance = new IPWhitelistService();
}
return IPWhitelistService.instance;
}
/**
* Check if IP is whitelisted for organization
*/
async isWhitelisted(
organizationId: number,
ipAddress: string
): Promise<boolean> {
const driver = await DBDriver.getInstance().getDriver(EDataSourceType.POSTGRESQL);
const manager = (await driver.getConcreteDriver()).manager;
// Get all active whitelist entries
const entries = await manager.find(DRAIPWhitelist, {
where: {
organization: { id: organizationId },
is_active: true
}
});
// If no entries, allow all (whitelist not enabled)
if (entries.length === 0) {
return true;
}
// Check if IP matches any entry
for (const entry of entries) {
if (this.matchesIPOrCIDR(ipAddress, entry.ip_address)) {
// Update usage stats
await this.recordUsage(entry.id);
return true;
}
}
return false;
}
/**
* Match IP against IP or CIDR range
*/
private matchesIPOrCIDR(requestIP: string, whitelistEntry: string): boolean {
try {
const addr = ipaddr.process(requestIP);
// Check if entry is CIDR notation
if (whitelistEntry.includes('/')) {
const [network, prefix] = whitelistEntry.split('/');
const networkAddr = ipaddr.process(network);
const prefixLength = parseInt(prefix);
return addr.match(networkAddr, prefixLength);
} else {
// Exact IP match
return requestIP === whitelistEntry;
}
} catch (error) {
console.error('IP matching error:', error);
return false;
}
}
/**
* Record IP usage
*/
private async recordUsage(whitelistId: number): Promise<void> {
const driver = await DBDriver.getInstance().getDriver(EDataSourceType.POSTGRESQL);
const manager = (await driver.getConcreteDriver()).manager;
await manager.increment(
DRAIPWhitelist,
{ id: whitelistId },
'usage_count',
1
);
await manager.update(DRAIPWhitelist, { id: whitelistId }, {
last_used_at: new Date()
});
}
/**
* Add IP to whitelist
*/
async addIP(
organizationId: number,
ipAddress: string,
description: string,
userId: number
): Promise<DRAIPWhitelist> {
// Validate IP/CIDR format
if (!this.isValidIPOrCIDR(ipAddress)) {
throw new Error('Invalid IP address or CIDR notation');
}
const driver = await DBDriver.getInstance().getDriver(EDataSourceType.POSTGRESQL);
const manager = (await driver.getConcreteDriver()).manager;
const entry = new DRAIPWhitelist();
entry.organization = { id: organizationId } as any;
entry.ip_address = ipAddress;
entry.description = description;
entry.created_by = { id: userId } as any;
return manager.save(entry);
}
/**
* Validate IP or CIDR format
*/
private isValidIPOrCIDR(input: string): boolean {
try {
if (input.includes('/')) {
const [ip, prefix] = input.split('/');
ipaddr.process(ip);
const prefixNum = parseInt(prefix);
return prefixNum >= 0 && prefixNum <= 32;
} else {
ipaddr.process(input);
return true;
}
} catch {
return false;
}
}
}
Middleware
// backend/src/middleware/ipWhitelist.ts
export function checkIPWhitelist() {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const organizationId = req.body.organizationContext?.organizationId;
if (!organizationId) {
return next();
}
const clientIP = req.ip || req.socket.remoteAddress || 'unknown';
const ipWhitelistService = IPWhitelistService.getInstance();
const isAllowed = await ipWhitelistService.isWhitelisted(organizationId, clientIP);
if (!isAllowed) {
console.warn(`❌ IP ${clientIP} not whitelisted for org ${organizationId}`);
// Log security event
await AuditLogService.getInstance().logEvent({
eventType: 'security',
action: 'ip_whitelist_violation',
userId: req.body.tokenDetails?.user_id,
ipAddress: clientIP,
metadata: { organizationId },
severity: 'warning',
success: false,
errorMessage: 'IP not whitelisted'
});
return res.status(403).json({
success: false,
message: 'Access denied: IP address not whitelisted',
code: 'IP_NOT_WHITELISTED'
});
}
next();
} catch (error) {
console.error('IP whitelist check error:', error);
// Fail open (allow access) on error to prevent lockouts
next();
}
};
}
// Apply to sensitive routes
app.use('/admin', authenticate, organizationContext(), checkIPWhitelist());
app.use('/data-source', authenticate, organizationContext(), checkIPWhitelist());
API Routes
// backend/src/routes/admin/ip_whitelist.ts
router.get('/ip-whitelist',
authenticate,
authorizeOrgAdmin(),
async (req, res) => {
const { organizationId } = req.body.organizationContext;
const driver = await DBDriver.getInstance().getDriver(EDataSourceType.POSTGRESQL);
const manager = (await driver.getConcreteDriver()).manager;
const entries = await manager.find(DRAIPWhitelist, {
where: { organization: { id: organizationId } },
relations: ['created_by'],
order: { created_at: 'DESC' }
});
res.json({ success: true, data: entries });
}
);
router.post('/ip-whitelist',
authenticate,
authorizeOrgAdmin(),
[
body('ipAddress').notEmpty().isString(),
body('description').optional().isString()
],
async (req, res) => {
try {
const { user_id } = req.body.tokenDetails;
const { organizationId } = req.body.organizationContext;
const { ipAddress, description } = req.body;
const ipWhitelistService = IPWhitelistService.getInstance();
const entry = await ipWhitelistService.addIP(
organizationId,
ipAddress,
description || '',
user_id
);
res.json({ success: true, data: entry });
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message
});
}
}
);
router.delete('/ip-whitelist/:id',
authenticate,
authorizeOrgAdmin(),
async (req, res) => {
const entryId = parseInt(req.params.id);
const { organizationId } = req.body.organizationContext;
const driver = await DBDriver.getInstance().getDriver(EDataSourceType.POSTGRESQL);
const manager = (await driver.getConcreteDriver()).manager;
await manager.delete(DRAIPWhitelist, {
id: entryId,
organization: { id: organizationId }
});
res.json({ success: true, message: 'IP removed from whitelist' });
}
);
Testing
describe('IPWhitelistService', () => {
it('should allow whitelisted IP', async () => {
await service.addIP(orgId, '192.168.1.100', 'Office', userId);
const allowed = await service.isWhitelisted(orgId, '192.168.1.100');
expect(allowed).toBe(true);
});
it('should block non-whitelisted IP', async () => {
await service.addIP(orgId, '192.168.1.100', 'Office', userId);
const allowed = await service.isWhitelisted(orgId, '192.168.1.200');
expect(allowed).toBe(false);
});
it('should support CIDR notation', async () => {
await service.addIP(orgId, '192.168.1.0/24', 'Office Network', userId);
const allowed = await service.isWhitelisted(orgId, '192.168.1.150');
expect(allowed).toBe(true);
});
it('should allow all if no whitelist entries', async () => {
const allowed = await service.isWhitelisted(orgId, '10.0.0.1');
expect(allowed).toBe(true);
});
});
Frontend UI
<template>
<div class="ip-whitelist-manager">
<h2>IP Whitelist</h2>
<p>Restrict access to specific IP addresses</p>
<div class="add-ip">
<input v-model="newIP" placeholder="192.168.1.0/24" />
<input v-model="description" placeholder="Description" />
<button @click="addIP">Add IP</button>
</div>
<table class="whitelist-table">
<thead>
<tr>
<th>IP/CIDR</th>
<th>Description</th>
<th>Last Used</th>
<th>Usage Count</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in entries" :key="entry.id">
<td>{{ entry.ip_address }}</td>
<td>{{ entry.description }}</td>
<td>{{ formatDate(entry.last_used_at) }}</td>
<td>{{ entry.usage_count }}</td>
<td>
<button @click="removeIP(entry.id)">Remove</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
Estimated Effort
- Database schema: 2 hours
- IP matching service: 4 hours
- Middleware: 3 hours
- API routes: 3 hours
- Frontend UI: 4 hours
- Testing: 4 hours
- Total: ~20 hours (2-3 developer days)
Success Criteria
Security Notes
- Fail Open: On error, allow access (prevent lockouts)
- Bypass for Superadmin: Platform admins bypass whitelist
- Emergency Access: Provide override mechanism
- Logging: Log all whitelist violations
Implement IP Whitelisting
Priority
LOW - Business+ tier, security feature
Labels
enhancement,security,backendDescription
Allow organizations to restrict API and dashboard access to specific IP addresses or CIDR ranges. Essential for enterprise security policies.
Current State
Implementation Overview
Database Schema
IP Matching Service
Middleware
API Routes
Testing
Frontend UI
Estimated Effort
Success Criteria
Security Notes