import {z} from 'zod'; import {PolicyGroupSchema,applyPolicyGroups,groupApplies} from '../../packages/core/policy-groups.mjs'; import {PolicySchema} from '../../packages/core/policy.mjs'; import {policyDiff} from './policy-history.mjs'; const conflict=message=>Object.assign(Error(message),{status:409}); export function validateGroupConfiguration(store,policy,groups=store.getRaw('policyGroups')||[],operation={}){ if(policy.assignment)throw Object.assign(Error('Global policy cannot include a device assignment'),{status:400}); groups=z.array(PolicyGroupSchema).max(100).parse(groups); if(new Set(groups.map(g=>g.id)).size!==groups.length)throw conflict('Duplicate policy group IDs'); const deviceIds=[...new Set(groups.flatMap(g=>g.memberDeviceIds))]; if(deviceIds.length>10000)throw conflict('Group assignment device budget exceeded'); const group=operation.type==='group-upsert'?PolicyGroupSchema.parse(operation.group):null; if(group)for(const id of group.memberDeviceIds){if(!store.db.prepare('SELECT id FROM devices WHERE id=? AND revoked=0').get(id))throw conflict('Group includes an unknown or revoked device');} for(const id of deviceIds){ let baseline=structuredClone(policy),device=store.db.prepare('SELECT document_analysis_enabled FROM devices WHERE id=?').get(id); if(device?.document_analysis_enabled!==null&&device?.document_analysis_enabled!==undefined)baseline.inspection.documentAnalysisEnabled=!!device.document_analysis_enabled; if(operation.type==='inspection'&&operation.deviceId===id)baseline.inspection.documentAnalysisEnabled=operation.enabled; let effective;try{effective=PolicySchema.parse(applyPolicyGroups(baseline,groups,id));}catch(error){if(error instanceof z.ZodError)throw error;throw conflict(error.message);} const personal=store.personalBookmarks(id); if(effective.browser.bookmarks.managed.some(b=>personal.items.some(p=>p.id===b.id)))throw conflict('Group company bookmark IDs conflict with personal bookmarks'); } return groups; } export function groupsForOperation(store,operation){ if(operation.type==='group-restore')return z.array(PolicyGroupSchema).max(100).parse(operation.groups); const groups=structuredClone(store.getRaw('policyGroups')||[]); if(operation.type==='group-upsert'){const group=PolicyGroupSchema.parse(operation.group),index=groups.findIndex(g=>g.id===group.id);if(index<0)groups.push(group);else groups[index]=group;} if(operation.type==='group-delete'){if(!groups.some(g=>g.id===operation.groupId))throw conflict('Policy group no longer exists');return groups.filter(g=>g.id!==operation.groupId);} return groups.sort((a,b)=>a.id.localeCompare(b.id,'en')); } export function installPolicyGroups(app,store,publisher){ app.get('/api/admin/policy-groups',(_req,res)=>{ const groups=store.getRaw('policyGroups')||[],version=store.get('policy').version; const devices=store.db.prepare('SELECT id,name,revoked,last_seen,policy_version FROM devices ORDER BY name,id').all().map(d=>({id:d.id,name:d.name,revoked:!!d.revoked,last_seen:d.last_seen,policyVersion:d.policy_version,targetVersion:version,acknowledged:d.policy_version===version,groups:groups.filter(g=>groupApplies(g,d.id)).map(g=>g.id),memberGroups:groups.filter(g=>g.memberDeviceIds.includes(d.id)).map(g=>g.id)})); res.json({version,groups,devices}); }); app.post('/api/admin/policy-groups/preview',(req,res)=>{ const b=z.object({deviceId:z.string().min(1).max(80),group:PolicyGroupSchema.optional()}).strict().parse(req.body); if(!store.db.prepare('SELECT id FROM devices WHERE id=? AND revoked=0').get(b.deviceId))return res.status(404).json({error:'Active device not found'}); const baseline=store.get('policy'),operation=b.group?{type:'group-upsert',group:b.group}:{},groups=groupsForOperation(store,operation); validateGroupConfiguration(store,baseline,groups,operation); const device=store.db.prepare('SELECT document_analysis_enabled FROM devices WHERE id=?').get(b.deviceId); if(device.document_analysis_enabled!==null)baseline.inspection.documentAnalysisEnabled=!!device.document_analysis_enabled; const policy=PolicySchema.parse(applyPolicyGroups(baseline,groups,b.deviceId)); res.json({policy,groups:policy.assignment.groups,diff:policyDiff(baseline,policy),currentDiff:policyDiff(store.effectivePolicy(b.deviceId),policy)}); }); app.put('/api/admin/policy-groups/:id',(req,res)=>{ const b=z.object({expectedVersion:z.number().int().positive(),group:PolicyGroupSchema}).strict().parse(req.body); if(b.group.id!==req.params.id)return res.status(400).json({error:'Group ID does not match route'}); const p=store.get('policy');if(p.version!==b.expectedVersion)return res.status(409).json({error:'Policy version conflict'}); p.version++;publisher.submit(req,res,p,'policy.group-published',{type:'group-upsert',group:b.group}); }); app.delete('/api/admin/policy-groups/:id',(req,res)=>{ const b=z.object({expectedVersion:z.number().int().positive()}).strict().parse(req.body),p=store.get('policy'); if(p.version!==b.expectedVersion)return res.status(409).json({error:'Policy version conflict'}); p.version++;publisher.submit(req,res,p,'policy.group-deleted',{type:'group-delete',groupId:req.params.id}); }); }