401 lines
11 KiB
TypeScript
401 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';
|
|
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.',
|
|
};
|
|
}
|
|
}
|