59 lines
5.1 KiB
JavaScript
59 lines
5.1 KiB
JavaScript
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});
|
||
|
|
});
|
||
|
|
}
|