Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions application/single_app/admin_settings_nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
{"id": "chat-file-uploads-section", "label": "Chat File Uploads", "icon": "bi-paperclip"},
{"id": "conversation-contents-drawer-section", "label": "Conversation Contents Drawer", "icon": "bi-list-nested"},
{"id": "workspace-scope-lock-section", "label": "Workspace Scope Lock", "icon": "bi-lock"},
{"id": "conversation-history-section", "label": "Conversation History", "icon": "bi-clock-history"},

Check warning on line 69 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
{"id": "default-system-prompt-section", "label": "Default System Prompt", "icon": "bi-chat-square-quote"},

Check warning on line 70 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
],
},
{
Expand Down Expand Up @@ -184,6 +186,7 @@
{"id": "file-download-settings-section", "label": "File Downloads", "icon": "bi-download"},
{"id": "file-sharing-section", "label": "File Sharing", "icon": "bi-share"},
{"id": "shared-conversation-file-approvals-section", "label": "Shared Conversation File Approvals", "icon": "bi-check2-square"},
{"id": "file-size-limit-section", "label": "Maximum File Size", "icon": "bi-file-earmark-arrow-up"},
],
},
{
Expand Down Expand Up @@ -284,6 +287,8 @@
"icon": "bi-person-check",
"sections": [
{"id": "permissions-section", "label": "Permissions", "icon": "bi-person-check"},
{"id": "app-role-requirements-section", "label": "App Role Requirements", "icon": "bi-person-badge"},

Check warning on line 290 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
{"id": "access-denied-message-section", "label": "Access Denied Message", "icon": "bi-shield-x"},
],
},
{
Expand All @@ -303,14 +308,14 @@
],
},
{
# Interim home for the mixed System Settings card. Four of its
# eight fields are idle-timeout, which is the plurality; the
# rest are split out to their real homes in a later change.
# Idle timeout is the only thing left here. The rest of the old
# System Settings card was split out to the tabs that own each
# setting, without renaming a single field.
"id": "session",
"label": "Session",
"icon": "bi-hourglass-split",
"sections": [
{"id": "system-settings-section", "label": "System Settings", "icon": "bi-sliders"},
{"id": "idle-timeout-section", "label": "Idle Session Timeout", "icon": "bi-hourglass-split"},
],
},
{
Expand Down Expand Up @@ -395,18 +400,50 @@
"icon": "bi-database",
"tabs": [
{
"id": "data-management",
"label": "Backup, Migrate & Restore",
"icon": "bi-database-check",
# Schedule, storage and encryption are cards nested inside the

Check warning on line 403 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
# backup card, so they stay with it.
"id": "backup",
"label": "Backup",
"icon": "bi-archive",
"sections": [
{"id": "data-management-readiness-section", "label": "Start Here", "icon": "bi-compass"},
{"id": "data-management-backup-section", "label": "Backup", "icon": "bi-archive"},
{"id": "data-management-schedule-section", "label": "Schedule", "icon": "bi-calendar-event"},
{"id": "data-management-storage-section", "label": "Storage", "icon": "bi-hdd"},
{"id": "data-management-encryption-section", "label": "Encryption", "icon": "bi-key"},
],
},
{
"id": "migrate",
"label": "Migrate",
"icon": "bi-arrow-left-right",
"sections": [
{"id": "data-management-migration-section", "label": "Migration", "icon": "bi-arrow-left-right"},
{"id": "data-management-cosmos-editor-section", "label": "Cosmos Editor", "icon": "bi-database-exclamation"},
],
},
{
"id": "restore",
"label": "Restore",
"icon": "bi-box-seam",
"sections": [
{"id": "data-management-backup-inventory-section", "label": "Backup Inventory & Restore", "icon": "bi-box-seam"},
],
},
{
# A direct database editor. It is a repair tool that belongs

Check warning on line 433 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
# with the backup and restore tooling it shares a module with.

Check warning on line 434 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"id": "cosmos-editor",

Check warning on line 435 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"label": "Cosmos Editor",

Check warning on line 436 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"icon": "bi-database-exclamation",
"sections": [
{"id": "data-management-cosmos-editor-section", "label": "Cosmos Editor", "icon": "bi-database-exclamation"},

Check warning on line 439 in application/single_app/admin_settings_nav.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
],
},
{
"id": "jobs",
"label": "Jobs",
"icon": "bi-clock-history",
"sections": [
{"id": "data-management-jobs-section", "label": "Jobs", "icon": "bi-clock-history"},
],
},
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.260.015"
VERSION = "0.260.017"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
148 changes: 148 additions & 0 deletions application/single_app/static/js/admin/admin_access_roles_roster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// admin_access_roles_roster.js

Check warning on line 1 in application/single_app/static/js/admin/admin_access_roles_roster.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
//
// Access & Roles shows every "require an Entra app role" switch in one place.
//
// The switches themselves stay on the tabs that own them, because that is where
// they make sense in context. Duplicating the real inputs here would submit
// each setting twice, so this builds a roster of mirrors instead: each row
// carries no name attribute and simply drives the canonical input.
//
// The roster is built from the page rather than from a hand-written list, so a
// new role requirement anywhere in Admin Settings appears here on its own and
// this list cannot fall out of step with reality.
//
// Roster links carry data-admin-link, which admin_card_links.js already handles
// through a delegated listener, so no wiring is needed here.

const ROLE_INPUT_SELECTOR = 'input[type="checkbox"][name^="require_member_of_"]';
const LIST_ID = 'app-role-requirements-list';
const EMPTY_ID = 'app-role-requirements-empty';
const ROSTER_CARD_ID = 'app-role-requirements-section';

/**
* Read the visible label for a control, falling back to its field name.
* @param {HTMLInputElement} input Canonical role checkbox.
* @returns {string} Human readable label.
*/
function labelFor(input) {
const explicit = input.id ? document.querySelector(`label[for="${input.id}"]`) : null;
if (explicit && explicit.textContent.trim()) {
return explicit.textContent.trim();
}

const wrapping = input.closest('label');
if (wrapping && wrapping.textContent.trim()) {
return wrapping.textContent.trim();
}

return input.name;
}

/**
* Find the card a control belongs to, so the roster can link back to it.
* @param {HTMLInputElement} input Canonical role checkbox.
* @returns {HTMLElement|null} The owning card, when it has an id.
*/
function owningCard(input) {
let card = input.closest('.card[id]');
while (card && card.id === ROSTER_CARD_ID) {
card = card.parentElement ? card.parentElement.closest('.card[id]') : null;
}
return card;
}

/**
* Read the heading of a card, used as the "where does this live" hint.
* @param {HTMLElement} card Owning card.
* @returns {string} Card title, or an empty string when it has none.
*/
function cardTitle(card) {
const heading = card ? card.querySelector('h5, h4, h6, .card-title') : null;
return heading ? heading.textContent.trim() : '';
}

/**
* Build one roster row: a mirror switch, its label, and a link to the setting.
* @param {HTMLInputElement} input Canonical role checkbox.
* @returns {HTMLElement} The row element.
*/
function buildRow(input) {
const row = document.createElement('div');
row.className = 'd-flex flex-wrap align-items-center gap-2';
row.setAttribute('data-role-requirement-row', input.name);

const wrapper = document.createElement('div');
wrapper.className = 'form-check form-switch mb-0 flex-grow-1';

// No name attribute: only the canonical input is submitted with the form.
const mirror = document.createElement('input');
mirror.type = 'checkbox';
mirror.className = 'form-check-input';
mirror.id = `${input.name}-roster-mirror`;
mirror.checked = input.checked;
mirror.disabled = input.disabled;
mirror.setAttribute('data-role-mirror-for', input.id || input.name);
mirror.setAttribute('data-ignore-settings-change', 'true');

const label = document.createElement('label');
label.className = 'form-check-label ms-2';
label.setAttribute('for', mirror.id);
label.textContent = labelFor(input);

wrapper.append(mirror, label);
row.appendChild(wrapper);

const card = owningCard(input);
if (card) {
const title = cardTitle(card);
const link = document.createElement('a');
link.href = `#${card.id}`;
link.className = 'small text-nowrap';
link.setAttribute('data-admin-link', card.id);
link.textContent = title ? `In ${title}` : 'Go to setting';
row.appendChild(link);
}

// Two-way: the mirror drives the real input, and the real input keeps the
// mirror honest when it is changed on its own tab.
mirror.addEventListener('change', () => {
if (input.checked === mirror.checked) {
return;
}
input.checked = mirror.checked;
input.dispatchEvent(new Event('change', { bubbles: true }));
});

input.addEventListener('change', () => {
mirror.checked = input.checked;
mirror.disabled = input.disabled;
});

return row;
}

/**
* Populate the Access & Roles roster from the role switches on the page.
*/
export function initAdminAccessRolesRoster() {
const list = document.getElementById(LIST_ID);
if (!list) {
return;
}

const empty = document.getElementById(EMPTY_ID);
const inputs = Array.from(document.querySelectorAll(ROLE_INPUT_SELECTOR))
.filter(input => !list.contains(input));

list.replaceChildren();
inputs
.map(input => ({ input, label: labelFor(input) }))
.sort((a, b) => a.label.localeCompare(b.label))
.forEach(({ input }) => list.appendChild(buildRow(input)));

if (empty) {
empty.classList.toggle('d-none', inputs.length > 0);
}
}

document.addEventListener('DOMContentLoaded', initAdminAccessRolesRoster);
44 changes: 44 additions & 0 deletions application/single_app/static/js/admin/admin_sidebar_nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,19 @@ function initAdminSidebarNav() {
}
} else {
console.log('initAdminSidebarNav - Found existing active tab, preserving current state:', activeTab.getAttribute('data-tab'));
syncAdminGroupSharedRegions(activeTab.getAttribute('data-tab'));
}

// Clicking a tab button directly does not go through showAdminTab, so the
// shared regions are synced from Bootstrap's own event as well.
document.querySelectorAll('button.nav-link[data-bs-target^="#"]').forEach(button => {
button.addEventListener('shown.bs.tab', event => {
const target = event.target.getAttribute('data-bs-target');
if (target) {
syncAdminGroupSharedRegions(target.slice(1));
}
});
});
}

function setupAdminGroupToggles() {
Expand Down Expand Up @@ -297,12 +309,43 @@ const LEGACY_TAB_REDIRECTS = {
'workspaces': 'workspace-types',
'search-extract': 'web-research',
'ai-models': 'model-endpoints',
'data-management': 'backup',
};

function resolveAdminTabId(tabId) {
return LEGACY_TAB_REDIRECTS[tabId] || tabId;
}

/**
* Some groups share one set of controls across all of their tabs, such as the
* single save button that serves every Backup & Recovery tab. Those controls
* cannot be duplicated into each pane without repeating element ids, and they
* cannot sit in one pane because the other tabs would lose them, so they live
* outside the panes and are revealed only while their group is active.
*/
function syncAdminGroupSharedRegions(tabId) {
const regions = document.querySelectorAll('[data-admin-group-shared]');
if (!regions.length) {
return;
}

// Only one of the two navigations is rendered at a time, so resolve the
// owning group from whichever is present. Looking only at the top tab strip
// would leave the region hidden for good in the sidebar layout.
const tabButton = document.querySelector(`.admin-tab-item[data-admin-group] button[data-bs-target="#${tabId}"]`);
let owner = tabButton ? tabButton.closest('[data-admin-group]') : null;
if (!owner) {
const sidebarLink = document.querySelector(`.admin-nav-tab[data-tab="${tabId}"]`);
owner = sidebarLink ? sidebarLink.closest('[data-admin-group]') : null;
}
const activeGroup = owner ? owner.getAttribute('data-admin-group') : null;

regions.forEach(region => {
const ownerGroup = region.getAttribute('data-admin-group-shared');
region.hidden = ownerGroup !== activeGroup;
});
}

function showAdminTab(requestedTabId) {
const tabId = resolveAdminTabId(requestedTabId);

Expand Down Expand Up @@ -333,6 +376,7 @@ function showAdminTab(requestedTabId) {

// Update the hash in URL for deep linking
window.location.hash = tabId;
syncAdminGroupSharedRegions(tabId);
if (typeof window.updateAdminSettingsSaveButtonState === 'function') {
window.updateAdminSettingsSaveButtonState();
}
Expand Down
32 changes: 31 additions & 1 deletion application/single_app/templates/admin/_panes/access-roles.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,37 @@ <h5>
</p>
</div>

<div class="card p-3 mb-3" id="app-role-requirements-section">
<h5>
<i class="bi bi-person-badge me-2"></i>App Role Requirements
</h5>
<p class="text-muted">
Every setting that can require an Entra app role, gathered here so the
full access policy can be read in one place. Each switch is a mirror of
the setting on its own tab, so changing it here changes it there.
</p>
{# Rows are built from the page itself by
admin_access_roles_roster.js, so a new role requirement appears here
automatically and this list can never fall out of step. #}
<div id="app-role-requirements-list" class="d-flex flex-column gap-2"></div>
<p class="text-muted small mb-0 d-none" id="app-role-requirements-empty">
No app role requirements are available on this page.
</p>
</div>


<div class="card p-3 mb-3" id="access-denied-message-section">
<h5>
<i class="bi bi-shield-x me-2"></i>Access Denied Message
</h5>
<p class="text-muted">
Shown to a user whose account does not carry a required role.
</p>
<div class="mb-3">
<label for="access_denied_message" class="form-label">Access Denied Message</label>
<small class="text-muted d-block mb-1">Shown to signed-in users who lack the required roles. Use Enter for line breaks.</small>
<textarea class="form-control" id="access_denied_message" name="access_denied_message"
rows="3">{{ settings.access_denied_message }}</textarea>
</div>
</div>

</div>
Loading
Loading