tenantpilot/lib/actions/policySettings.ts
Ahmed Darrazi 51a76ef944
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 1s
fix: Remove .env copy and fix TypeScript errors in worker build
2025-12-09 13:38:13 +01:00

400 lines
11 KiB
TypeScript

'use server';
import { db, policySettings, type PolicySetting } from '@/lib/db';
import { getUserAuth } from '@/lib/auth/utils';
import { eq, ilike, or, desc, and, ne, isNotNull } from 'drizzle-orm';
import { env } from '@/lib/env.mjs';
import { syncQueue } from '@/lib/queue/syncQueue';
export interface PolicySettingSearchResult {
id: string;
policyName: string;
policyType: string;
settingName: string;
settingValue: string;
lastSyncedAt: Date;
}
export interface SearchResult {
success: boolean;
data?: PolicySettingSearchResult[];
error?: string;
totalCount?: number;
}
export interface GetSettingResult {
success: boolean;
data?: PolicySetting;
error?: string;
}
export interface RecentSettingsResult {
success: boolean;
data?: PolicySettingSearchResult[];
error?: string;
}
export interface AllSettingsResult {
success: boolean;
data?: PolicySettingSearchResult[];
error?: string;
totalCount?: number;
}
/**
* Search policy settings by keyword across settingName and settingValue
*
* **Security**: This function enforces tenant isolation by:
* 1. Validating user session via getUserAuth()
* 2. Extracting tenantId from session
* 3. Including explicit WHERE tenantId = ? in ALL queries
*
* @param searchTerm - Search query (min 2 characters)
* @param limit - Maximum number of results (default 100, max 200)
* @returns Search results filtered by user's tenant
*/
export async function searchPolicySettings(
searchTerm: string,
limit: number = 100
): Promise<SearchResult> {
try {
const { session } = await getUserAuth();
// T017: Explicit security check - must have authenticated session
if (!session?.user) {
return { success: false, error: 'Unauthorized' };
}
// T017: Explicit security check - must have tenantId in session
const tenantId = session.user.tenantId;
if (!tenantId) {
return { success: false, error: 'Tenant not found' };
}
if (searchTerm.length < 2) {
return { success: false, error: 'Search term too short (min 2 characters)' };
}
// Limit search term length to prevent abuse
const sanitizedSearchTerm = searchTerm.slice(0, 200);
const searchPattern = `%${sanitizedSearchTerm}%`;
// Enforce maximum limit
const safeLimit = Math.min(Math.max(1, limit), 200);
// Explicit WHERE clause filters by tenantId FIRST for security + null filtering
const results = await db
.select({
id: policySettings.id,
policyName: policySettings.policyName,
policyType: policySettings.policyType,
settingName: policySettings.settingName,
settingValue: policySettings.settingValue,
lastSyncedAt: policySettings.lastSyncedAt,
})
.from(policySettings)
.where(
and(
eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation
ne(policySettings.settingValue, 'null'), // Filter out string "null"
ne(policySettings.settingValue, ''), // Filter out empty strings
isNotNull(policySettings.settingValue), // Filter out NULL values
or(
ilike(policySettings.settingName, searchPattern),
ilike(policySettings.settingValue, searchPattern)
)
)
)
.orderBy(policySettings.settingName)
.limit(safeLimit);
return {
success: true,
data: results,
totalCount: results.length,
};
} catch (error) {
console.error('Search failed:', error);
return { success: false, error: 'Search failed' };
}
}
/**
* Get a single policy setting by ID
*
* **Security**: Enforces tenant isolation with explicit WHERE tenantId filter
*
* @param id - Policy setting ID
* @returns Policy setting if found and belongs to user's tenant
*/
export async function getPolicySettingById(
id: string
): Promise<GetSettingResult> {
try {
const { session } = await getUserAuth();
// T017: Explicit security check
if (!session?.user) {
return { success: false, error: 'Unauthorized' };
}
// T017: Explicit security check
const tenantId = session.user.tenantId;
if (!tenantId) {
return { success: false, error: 'Tenant not found' };
}
// T017: Query filtered by tenantId FIRST for security
const [result] = await db
.select()
.from(policySettings)
.where(
and(
eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation
eq(policySettings.id, id)
)
)
.limit(1);
if (!result) {
return { success: false, error: 'Policy setting not found' };
}
return {
success: true,
data: result,
};
} catch (error) {
console.error('Get policy setting failed:', error);
return { success: false, error: 'Failed to fetch policy setting' };
}
}
/**
* Get recent policy settings sorted by last sync date
*
* **Security**: Enforces tenant isolation with explicit WHERE tenantId filter
*
* @param limit - Maximum number of results (1-100, default 50)
* @returns Recent policy settings for user's tenant
*/
export async function getRecentPolicySettings(
limit: number = 50
): Promise<RecentSettingsResult> {
try {
const { session } = await getUserAuth();
// T017: Explicit security check
if (!session?.user) {
return { success: false, error: 'Unauthorized' };
}
// T017: Explicit security check
const tenantId = session.user.tenantId;
if (!tenantId) {
return { success: false, error: 'Tenant not found' };
}
// Clamp limit between 1 and 100
const safeLimit = Math.max(1, Math.min(100, limit));
// T017: Query filtered by tenantId for security
const results = await db
.select({
id: policySettings.id,
policyName: policySettings.policyName,
policyType: policySettings.policyType,
settingName: policySettings.settingName,
settingValue: policySettings.settingValue,
lastSyncedAt: policySettings.lastSyncedAt,
})
.from(policySettings)
.where(eq(policySettings.tenantId, tenantId)) // CRITICAL: Tenant isolation
.orderBy(desc(policySettings.lastSyncedAt))
.limit(safeLimit);
return {
success: true,
data: results,
};
} catch (error) {
console.error('Get recent settings failed:', error);
return { success: false, error: 'Failed to fetch recent settings' };
}
}
/**
* Get all policy settings for the current user's tenant
*
* **Security**: Enforces tenant isolation with explicit WHERE tenantId filter
*
* @returns All policy settings for user's tenant
*/
export async function getAllPolicySettings(): Promise<AllSettingsResult> {
try {
const { session } = await getUserAuth();
// T017: Explicit security check
if (!session?.user) {
return { success: false, error: 'Unauthorized' };
}
// T017: Explicit security check
const tenantId = session.user.tenantId;
if (!tenantId) {
return { success: false, error: 'Tenant not found' };
}
// T017: Query filtered by tenantId for security
const results = await db
.select({
id: policySettings.id,
policyName: policySettings.policyName,
policyType: policySettings.policyType,
settingName: policySettings.settingName,
settingValue: policySettings.settingValue,
lastSyncedAt: policySettings.lastSyncedAt,
})
.from(policySettings)
.where(eq(policySettings.tenantId, tenantId)) // CRITICAL: Tenant isolation
.orderBy(desc(policySettings.lastSyncedAt));
return {
success: true,
data: results,
totalCount: results.length,
};
} catch (error) {
console.error('Get all settings failed:', error);
return { success: false, error: 'Failed to fetch settings' };
}
}
/**
* TEMPORARY: Seed test data for the current user's tenant
* This is a development helper to populate realistic policy settings
*/
export async function seedMyTenantData(): Promise<{
success: boolean;
error?: string;
message?: string;
}> {
try {
const { session } = await getUserAuth();
if (!session?.user) {
return { success: false, error: 'Unauthorized' };
}
const tenantId = session.user.tenantId;
if (!tenantId) {
return { success: false, error: 'Tenant ID not found in session' };
}
// Create 5 realistic policy settings for the user's tenant
const seedData = [
{
tenantId,
policyName: 'Windows 10 Security Baseline',
policyType: 'deviceConfiguration' as const,
settingName: 'USB.BlockExternalDevices',
settingValue: 'enabled',
graphPolicyId: `seed-${tenantId}-policy-001`,
},
{
tenantId,
policyName: 'BitLocker Compliance Policy',
policyType: 'compliancePolicy' as const,
settingName: 'BitLocker.RequireEncryption',
settingValue: 'true',
graphPolicyId: `seed-${tenantId}-policy-002`,
},
{
tenantId,
policyName: 'Camera and Microphone Restrictions',
policyType: 'deviceConfiguration' as const,
settingName: 'Camera.DisableCamera',
settingValue: 'false',
graphPolicyId: `seed-${tenantId}-policy-003`,
},
{
tenantId,
policyName: 'Windows Defender Configuration',
policyType: 'endpointSecurity' as const,
settingName: 'Defender.EnableRealTimeProtection',
settingValue: 'enabled',
graphPolicyId: `seed-${tenantId}-policy-004`,
},
{
tenantId,
policyName: 'Windows Update for Business',
policyType: 'windowsUpdateForBusiness' as const,
settingName: 'WindowsUpdate.DeferFeatureUpdatesPeriodInDays',
settingValue: '30',
graphPolicyId: `seed-${tenantId}-policy-005`,
},
];
// Insert all seed data
for (const data of seedData) {
await db.insert(policySettings).values({
...data,
lastSyncedAt: new Date(),
});
}
return {
success: true,
message: `Successfully seeded 5 policy settings for tenant ${tenantId}`,
};
} catch (error) {
console.error('Seed data failed:', error);
return { success: false, error: 'Failed to seed data' };
}
}
/**
* Trigger manual policy sync via BullMQ worker
*
* **Security**: This function enforces tenant isolation by:
* 1. Validating user session via getUserAuth()
* 2. Extracting tenantId from session
* 3. Enqueuing a job with only the authenticated user's tenantId
*
* @returns Success/error result with job ID
*/
export async function triggerPolicySync(): Promise<{ success: boolean; message?: string; error?: string; jobId?: string }> {
try {
const { session } = await getUserAuth();
if (!session?.user) {
return { success: false, error: 'Not authenticated' };
}
const tenantId = session.user.tenantId;
if (!tenantId) {
return { success: false, error: 'No tenant ID found in session' };
}
// Enqueue sync job to BullMQ
const job = await syncQueue.add('sync-tenant', {
tenantId,
source: 'manual_trigger',
triggeredAt: new Date().toISOString(),
triggeredBy: session.user.email || session.user.id,
});
return {
success: true,
message: `Policy sync queued successfully (Job #${job.id})`,
jobId: job.id,
};
} catch (error) {
console.error('Failed to trigger policy sync:', error);
return {
success: false,
error: 'Failed to queue sync job. Please try again later.',
};
}
}