/* 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','agentBackgroundWorkers']; 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();});