Files
safetynet/apps/server/public/profile-admin.js
T

49 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Profiles add restrictions; reviewed developer grants require central delegation. */
let profileState={version:0,profiles:[],categories:[],delegation:{devTools:false},devices:[]},selectedProfile='';
const profileFeatureNames=['devTools','downloads','ai','agents'];
function profileDraft(){return {id:$('profile-id').value.trim(),name:$('profile-name').value.trim(),subjects:lines('profile-subjects'),deviceIds:groupSelected('profile-devices'),allowedHosts:lines('profile-allowed'),blockedHosts:lines('profile-blocked'),blockedCategoryIds:groupSelected('profile-categories'),features:Object.fromEntries(profileFeatureNames.map(key=>[key,$(key==='devTools'?'profile-devtools':`profile-${key}`).value])),requireDocumentScan:$('profile-scan').checked};}
function editProfile(profile){selectedProfile=profile?.id||'';$('profile-editor-title').textContent=profile?`Edit ${profile.name}`:'Create profile';$('profile-id').value=profile?.id||'';$('profile-id').readOnly=!!profile;$('profile-name').value=profile?.name||'';$('profile-subjects').value=(profile?.subjects||[]).join('\n');$('profile-allowed').value=(profile?.allowedHosts||[]).join('\n');$('profile-blocked').value=(profile?.blockedHosts||[]).join('\n');groupChecks('profile-devices',profileState.devices,profile?.deviceIds||[]);groupChecks('profile-categories',profileState.categories,profile?.blockedCategoryIds||[]);for(const key of profileFeatureNames)$(key==='devTools'?'profile-devtools':`profile-${key}`).value=profile?.features?.[key]||'inherit';$('profile-scan').checked=profile?.requireDocumentScan===true;$('profile-preview-summary').textContent='';$('profile-preview-diff').replaceChildren();applyProfilePermissions(currentPrincipal);}
function applyProfilePermissions(principal){if(!principal)return;const permissions=new Set(principal.permissions||[]),write=permissions.has('policy.write')||(independentApprovalRequired&&permissions.has('policy.prepare'));for(const field of $('user-profiles').querySelectorAll('input,textarea,select,button'))field.disabled=!write;for(const id of ['profiles-refresh','profile-preview-device','profile-preview'])$(id).disabled=!permissions.has('policy.read');for(const button of $('profiles-rows').querySelectorAll('button'))button.disabled=!permissions.has('policy.read');for(const button of $('profile-category-editor').querySelectorAll('button'))if(button.dataset.categoryReferenced==='true')button.disabled=true;$('profile-delete').disabled=!write||!selectedProfile;$('profile-save').textContent=independentApprovalRequired?'Prepare profile for independent approval':'Publish profile';$('profile-config-save').textContent=independentApprovalRequired?'Prepare categories and delegation for approval':'Publish categories and delegation';}
async function loadUserProfiles(){profileState=await api('/api/admin/user-profiles');$('profiles-rows').replaceChildren();for(const profile of profileState.profiles){const row=document.createElement('tr');cell(row,profile.name);cell(row,`${profile.subjects.length} verified subjects / ${profile.deviceIds.length} devices`);cell(row,profile.features.devTools);const button=document.createElement('button');button.className='secondary';button.textContent='Edit profile';button.onclick=()=>editProfile(profile);cell(row,'').append(button);$('profiles-rows').append(row);}$('profile-preview-device').replaceChildren();$('profile-device-rows').replaceChildren();for(const device of profileState.devices){const row=document.createElement('tr');cell(row,device.name);cell(row,device.verifiedSubject?device.subject:'No verified subject; explicit device assignment only');cell(row,(device.profiles||[]).join(', ')||'Baseline');$('profile-device-rows').append(row);if(!device.revoked){const option=document.createElement('option');option.value=device.id;option.textContent=device.name;$('profile-preview-device').append(option);}}$('profiles-status').textContent=`${profileState.profiles.length} profiles / signed policy v${profileState.version}`;if(!categoryDraftDirty)$('profile-delegation').checked=profileState.delegation.devTools===true;if(!categoryDraftDirty){categoryDraft=structuredClone(profileState.categories);categoryDraftVersion=profileState.version;renderCategories();}else categoryStatus('Unsaved category draft retained. Refresh does not discard it.');editProfile(profileState.profiles.find(profile=>profile.id===selectedProfile));}
$('profiles-refresh').onclick=task(loadUserProfiles);$('profile-new').onclick=()=>editProfile();
$('profile-use-template').onclick=()=>{const kind=$('profile-template').value;editProfile({id:kind,name:kind==='developer'?'Developers':kind==='employee'?'Employees':'High protection',features:{devTools:kind==='developer'?'allow':'deny',downloads:kind==='high-protection'?'deny':'inherit',ai:'inherit',agents:kind==='high-protection'?'deny':'inherit'},requireDocumentScan:kind==='high-protection'});status('Class loaded into local draft. Assign verified subjects or managed devices and review before publication.');};
$('profile-save').onclick=task(async()=>{const profile=profileDraft();if(!profile.id||!profile.name)throw Error('Enter profile identifier and name');const result=await api(`/api/admin/user-profiles/${encodeURIComponent(profile.id)}`,{expectedVersion:profileState.version,profile},'PUT');selectedProfile=profile.id;await load();policyMutationStatus(result,`Profile published as signed policy v${result.version}.`);});
$('profile-delete').onclick=task(async()=>{if(!selectedProfile)throw Error('Select a published profile');const result=await api(`/api/admin/user-profiles/${encodeURIComponent(selectedProfile)}`,{expectedVersion:profileState.version},'DELETE');selectedProfile='';await load();policyMutationStatus(result,`Profile removed as signed policy v${result.version}.`);});
$('profile-config-save').onclick=task(async()=>{const categories=validateCategoryDraft(categoryDraft),result=await api('/api/admin/user-profile-configuration',{expectedVersion:categoryDraftVersion,categories,delegation:{devTools:$('profile-delegation').checked}},'PUT');categoryDraftDirty=false;await load();policyMutationStatus(result,`Categories and delegation published as signed policy v${result.version}.`);});
$('profile-preview').onclick=task(async()=>{const profile=profileDraft(),body={deviceId:$('profile-preview-device').value};if(profile.id)body.profile=profile;const result=await api('/api/admin/user-profiles/preview',body);$('profile-preview-summary').textContent=`Effective profiles: ${(result.profiles||[]).join(', ')||'baseline only'} / ${result.diff?.changes?.length||0} policy differences`;$('profile-preview-diff').replaceChildren();for(const change of result.diff?.changes||[]){const row=document.createElement('article');row.className='change-item';const title=document.createElement('strong'),before=document.createElement('pre'),after=document.createElement('pre');title.textContent=change.path;before.textContent=`Baseline: ${JSON.stringify(change.before)}`;after.textContent=`Effective: ${JSON.stringify(change.after)}`;row.append(title,before,after);$('profile-preview-diff').append(row);}if(result.diff?.truncated){const warning=document.createElement('p');warning.textContent='Diff truncated. Review the complete effective policy through the authenticated API.';$('profile-preview-diff').append(warning);}});
// Structured category controls use textContent, never HTML from administrator input.
let categoryDraft=[],categoryDraftDirty=false,categoryDraftVersion=0;
function categoryStatus(message){$('profile-category-status').textContent=message;}
function validateCategoryDraft(categories){
if(categories.length>100)throw Error('At most 100 categories are supported.');
const ids=new Set();
for(const category of categories){
if(!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(category.id))throw Error('Category identifier must contain 1–80 letters, digits, dots, underscores or hyphens.');
if(ids.has(category.id))throw Error('Category identifiers must be unique.');ids.add(category.id);
if(!category.name.length||category.name.length>100)throw Error('Category name must contain 1–100 characters.');
if(category.hosts.length>500||new Set(category.hosts).size!==category.hosts.length)throw Error('Use at most 500 unique hosts per category.');
for(const host of category.hosts)if(host.length>253||!/^(\*\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i.test(host))throw Error('Enter hosts only, without a scheme, path, port or spaces.');
}
return structuredClone(categories);
}
function categoryChanged(){categoryDraftDirty=true;categoryStatus(`Unsaved category draft based on policy v${categoryDraftVersion}. Publish explicitly after review.`);}
function categoryReferences(id){return profileState.profiles.filter(profile=>profile.blockedCategoryIds.includes(id));}
function renderCategories(){
const container=$('profile-category-editor');container.replaceChildren();
categoryDraft.forEach((category,index)=>{
const fieldset=document.createElement('fieldset'),legend=document.createElement('legend');legend.textContent=`Category ${index+1}`;fieldset.append(legend);
for(const [key,title] of [['id','Stable identifier'],['name','Display name'],['hosts','Website hosts — one per line']]){
const label=document.createElement('label'),input=document.createElement(key==='hosts'?'textarea':'input');label.textContent=title;input.id=`category-${index}-${key}`;input.value=key==='hosts'?category.hosts.join('\n'):category[key];
if(key==='id'){input.maxLength=80;input.readOnly=profileState.categories.some(item=>item.id===category.id);}else if(key==='name')input.maxLength=100;
input.oninput=()=>{category[key]=key==='hosts'?input.value.split(/\r?\n/).map(value=>value.trim()).filter(Boolean):input.value.trim();categoryChanged();};label.append(input);fieldset.append(label);
}
const references=categoryReferences(category.id),note=document.createElement('p'),remove=document.createElement('button');note.textContent=references.length?`Used by: ${references.map(profile=>profile.name).join(', ')}. Remove these assignments and publish them before deleting this category.`:'Not assigned to a published profile.';remove.textContent='Remove category from draft';remove.className='danger';remove.dataset.categoryReferenced=references.length?'true':'false';remove.onclick=()=>{if(categoryReferences(category.id).length){categoryStatus('Category is still referenced by a published profile. Remove its assignments first.');return;}categoryDraft.splice(index,1);categoryChanged();renderCategories();};fieldset.append(note,remove);container.append(fieldset);
});
applyProfilePermissions(currentPrincipal);
for(const button of container.querySelectorAll('button'))if(button.dataset.categoryReferenced==='true')button.disabled=true;
}
$('profile-category-add').onclick=()=>{if(categoryDraft.length>=100){categoryStatus('At most 100 categories are supported.');return;}categoryDraft.push({id:'',name:'',hosts:[]});categoryChanged();renderCategories();};
$('profile-category-reset').onclick=()=>{categoryDraft=structuredClone(profileState.categories);categoryDraftVersion=profileState.version;categoryDraftDirty=false;$('profile-delegation').checked=profileState.delegation.devTools===true;renderCategories();categoryStatus('Category draft discarded; published categories loaded.');};
$('profile-delegation').addEventListener('change',()=>{if(!categoryDraftDirty)categoryDraftVersion=profileState.version;categoryChanged();});