Skip to content
Open
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
242 changes: 117 additions & 125 deletions src/Frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"prettier": "3.8.3",
"typescript": "6.0.3",
"typescript-eslint": "8.60.0",
"vite": "8.0.14",
"vite": "8.0.16",
"vite-plugin-checker": "0.14.1",
"vite-plugin-vue-devtools": "8.1.2",
"vitest": "4.1.7",
Expand Down
14 changes: 13 additions & 1 deletion src/Frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, watch } from "vue";
import { RouterView, useRoute } from "vue-router";
import PageFooter from "./components/PageFooter.vue";
import PageHeader from "./components/PageHeader.vue";
Expand All @@ -8,11 +8,23 @@ import LicenseNotifications from "@/components/LicenseNotifications.vue";
import BackendChecksNotifications from "@/components/BackendChecksNotifications.vue";
import { storeToRefs } from "pinia";
import { useAuthStore } from "@/stores/AuthStore";
import { useUserPermissionsStore } from "@/stores/UserPermissionsStore";

const authStore = useAuthStore();
const route = useRoute();
const { isAuthenticated, authEnabled } = storeToRefs(authStore);

const permissionsStore = useUserPermissionsStore();
watch(
[authEnabled, isAuthenticated],
([enabled, authenticated]) => {
if (enabled && authenticated) {
permissionsStore.refresh();
}
},
{ immediate: true }
);

// Check if the current route allows anonymous access (e.g., logged-out page)
const isAnonymousRoute = computed(() => route.meta?.allowAnonymous === true);
const shouldShowApp = computed(() => !authEnabled.value || isAuthenticated.value || isAnonymousRoute.value);
Expand Down
24 changes: 17 additions & 7 deletions src/Frontend/src/components/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,34 @@ import AuditMenuItem from "./audit/AuditMenuItem.vue";
import monitoringClient from "@/components/monitoring/monitoringClient";
import UserProfileMenuItem from "@/components/UserProfileMenuItem.vue";
import { useAuthStore } from "@/stores/AuthStore";
import { useUserPermissionsStore, type PermissionsSummary } from "@/stores/UserPermissionsStore";
import { storeToRefs } from "pinia";

const isMonitoringEnabled = monitoringClient.isMonitoringEnabled;

const authStore = useAuthStore();
const { authEnabled, isAuthenticated } = storeToRefs(authStore);

const permissionsStore = useUserPermissionsStore();
const { summary } = storeToRefs(permissionsStore);

const shouldGate = computed(() => authEnabled.value && isAuthenticated.value && summary.value !== null);

function has(flag: keyof PermissionsSummary): boolean {
return !shouldGate.value || summary.value?.[flag] === true;
}

// prettier-ignore
const menuItems = computed(
() => [
DashboardMenuItem,
HeartbeatsMenuItem,
...(isMonitoringEnabled ? [MonitoringMenuItem] : []),
AuditMenuItem,
FailedMessagesMenuItem,
CustomChecksMenuItem,
EventsMenuItem,
ThroughputMenuItem,
...(has("failed_messages_read") ? [HeartbeatsMenuItem] : []),
...(isMonitoringEnabled && has("monitoring_read") ? [MonitoringMenuItem] : []),
...(has("auditing_read") ? [AuditMenuItem] : []),
...(has("failed_messages_read") ? [FailedMessagesMenuItem] : []),
...(has("failed_messages_read") ? [CustomChecksMenuItem] : []),
...(has("failed_messages_read") ? [EventsMenuItem] : []),
...(has("failed_messages_read") ? [ThroughputMenuItem] : []),
ConfigurationMenuItem,
FeedbackButton,
]);
Expand Down
124 changes: 124 additions & 0 deletions src/Frontend/src/components/configuration/UserPermissions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
import FAIcon from "@/components/FAIcon.vue";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import { useUserPermissionsStore } from "@/stores/UserPermissionsStore";

const store = useUserPermissionsStore();

onMounted(async () => {
await store.refresh();
});
</script>

<template>
<section name="user-permissions">
<div class="box">
<LoadingSpinner v-if="store.loading" />
<div v-else-if="store.error" class="alert alert-danger">{{ store.error }}</div>
<template v-else-if="store.summary && store.descriptor">
<div class="row">
<div class="col-12">
<h3>Permissions Summary</h3>
<table class="table permissions-table">
<thead>
<tr>
<th>Area</th>
<th class="text-center">Read</th>
<th class="text-center">Write</th>
</tr>
</thead>
<tbody>
<tr>
<td>Failed Messages</td>
<td class="text-center">
<FAIcon :icon="store.summary.failed_messages_read ? faCheck : faTimes" :class="store.summary.failed_messages_read ? 'icon-granted' : 'icon-denied'" />
</td>
<td class="text-center">
<FAIcon :icon="store.summary.failed_messages_write ? faCheck : faTimes" :class="store.summary.failed_messages_write ? 'icon-granted' : 'icon-denied'" />
</td>
</tr>
<tr>
<td>Auditing</td>
<td class="text-center">
<FAIcon :icon="store.summary.auditing_read ? faCheck : faTimes" :class="store.summary.auditing_read ? 'icon-granted' : 'icon-denied'" />
</td>
<td class="text-center">—</td>
</tr>
<tr>
<td>Monitoring</td>
<td class="text-center">
<FAIcon :icon="store.summary.monitoring_read ? faCheck : faTimes" :class="store.summary.monitoring_read ? 'icon-granted' : 'icon-denied'" />
</td>
<td class="text-center">
<FAIcon :icon="store.summary.monitoring_write ? faCheck : faTimes" :class="store.summary.monitoring_write ? 'icon-granted' : 'icon-denied'" />
</td>
</tr>
<tr>
<td>Admin</td>
<td class="text-center">
<FAIcon :icon="store.summary.admin_read ? faCheck : faTimes" :class="store.summary.admin_read ? 'icon-granted' : 'icon-denied'" />
</td>
<td class="text-center">
<FAIcon :icon="store.summary.admin_write ? faCheck : faTimes" :class="store.summary.admin_write ? 'icon-granted' : 'icon-denied'" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-12">
<h3>All Permissions</h3>
<p class="user-label">User: {{ store.descriptor.user }}</p>
<ul class="permissions-list">
<li v-for="permission in store.descriptor.permissions" :key="permission">{{ permission }}</li>
</ul>
</div>
</div>
</template>
</div>
</section>
</template>

<style scoped>
.permissions-table {
max-width: 480px;
}

.permissions-table th,
.permissions-table td {
padding: 10px 16px;
}

.icon-granted {
color: #28a745;
}

.icon-denied {
color: #dc3545;
}

.user-label {
color: #666;
margin-bottom: 12px;
}

.permissions-list {
list-style: none;
padding: 0;
margin: 0;
font-family: monospace;
font-size: 13px;
}

.permissions-list li {
padding: 4px 0;
border-bottom: 1px solid #f0f0f0;
}

.permissions-list li:last-child {
border-bottom: none;
}
</style>
2 changes: 2 additions & 0 deletions src/Frontend/src/resources/RootUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ export default interface RootUrls {
event_log_items: string;
archived_groups_url: string;
get_archive_group: string;
mypermissions_all?: string;
mypermissions_summary?: string;
}
5 changes: 5 additions & 0 deletions src/Frontend/src/router/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ const config: RouteItem[] = [
path: routeLinks.configuration.endpointConnection.template,
component: () => import("@/components/configuration/EndpointConnection.vue"),
},
{
title: "User Permissions",
path: routeLinks.configuration.userPermissions.template,
component: () => import("@/components/configuration/UserPermissions.vue"),
},
{
title: "Usage Setup",
path: routeLinks.throughput.setup.root,
Expand Down
1 change: 1 addition & 0 deletions src/Frontend/src/router/routeLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const configurationLinks = (root: string) => {
retryRedirects: createLink("retry-redirects"),
connections: createLink("connections"),
endpointConnection: createLink("endpoint-connection"),
userPermissions: createLink("user-permissions"),
};
};

Expand Down
2 changes: 2 additions & 0 deletions src/Frontend/src/stores/EnvironmentAndVersionsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const useEnvironmentAndVersionsStore = defineStore("EnvironmentAndVersion
is_compatible_with_sc: true,
sp_version: window.defaultConfig && window.defaultConfig.version ? window.defaultConfig.version : "1.2.0",
supportsArchiveGroups: false,
supportsUserPermissions: false,
endpoints_error_url: "",
known_endpoints_url: "",
endpoints_message_search_url: "",
Expand Down Expand Up @@ -56,6 +57,7 @@ export const useEnvironmentAndVersionsStore = defineStore("EnvironmentAndVersion
const [products, scVer] = await Promise.all([productsResult, scResult, mResult]);
if (scVer) {
environment.supportsArchiveGroups = !!scVer.archived_groups_url;
environment.supportsUserPermissions = !!scVer.mypermissions_all && !!scVer.mypermissions_summary;
environment.is_compatible_with_sc = isSupported(environment.sc_version, environment.minimum_supported_sc_version);
environment.endpoints_error_url = scVer && scVer.endpoints_error_url;
environment.known_endpoints_url = scVer && scVer.known_endpoints_url;
Expand Down
50 changes: 50 additions & 0 deletions src/Frontend/src/stores/UserPermissionsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { acceptHMRUpdate, defineStore } from "pinia";
import { ref } from "vue";
import serviceControlClient from "@/components/serviceControlClient";

interface PermissionsSummary {
failed_messages_read: boolean;
failed_messages_write: boolean;
auditing_read: boolean;
monitoring_read: boolean;
monitoring_write: boolean;
admin_read: boolean;
admin_write: boolean;
}

interface PermissionsDescriptor {
user: string;
permissions: string[];
}

export const useUserPermissionsStore = defineStore("UserPermissionsStore", () => {
const summary = ref<PermissionsSummary | null>(null);
const descriptor = ref<PermissionsDescriptor | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);

async function refresh() {
loading.value = true;
error.value = null;
try {
const [summaryResult, descriptorResult] = await Promise.all([
serviceControlClient.fetchTypedFromServiceControl<PermissionsSummary>("my/permissions"),
serviceControlClient.fetchTypedFromServiceControl<PermissionsDescriptor>("my/permissions/all"),
]);
summary.value = summaryResult[1];
descriptor.value = descriptorResult[1];
} catch {
error.value = "Failed to load user permissions";
} finally {
loading.value = false;
}
}

return { summary, descriptor, loading, error, refresh };
});

export type { PermissionsSummary, PermissionsDescriptor };

if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserPermissionsStore, import.meta.hot));
}
Loading