diff --git a/assets/js/common/DisabledGuard/DisabledGuard.jsx b/assets/js/common/DisabledGuard/DisabledGuard.jsx
index bde1e11b40..f933477a14 100644
--- a/assets/js/common/DisabledGuard/DisabledGuard.jsx
+++ b/assets/js/common/DisabledGuard/DisabledGuard.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { isPermitted } from '@lib/model/users';
import Tooltip from '@common/Tooltip';
@@ -22,11 +23,7 @@ function DisabledGuard({
}) {
const permittedFor = ALL_PERMITTED.concat(permitted);
- const isPermitted = userAbilities
- .map(({ name, resource }) => `${name}:${resource}`)
- .some((ability) => permittedFor.includes(ability));
-
- if (isPermitted) {
+ if (isPermitted(userAbilities, permittedFor)) {
return children;
}
diff --git a/assets/js/lib/model/activityLog.js b/assets/js/lib/model/activityLog.js
index 5c91b9988d..743633f133 100644
--- a/assets/js/lib/model/activityLog.js
+++ b/assets/js/lib/model/activityLog.js
@@ -1,3 +1,7 @@
+import { has } from 'lodash';
+import { entries, filter, get, map, pipe } from 'lodash/fp';
+import { isPermitted } from './users';
+
export const LOGIN_ATTEMPT = 'login_attempt';
export const RESOURCE_TAGGING = 'resource_tagging';
export const RESOURCE_UNTAGGING = 'resource_untagging';
@@ -98,88 +102,6 @@ export const DATABASE_ROLL_UP_REQUESTED = 'database_roll_up_requested';
export const DATABASE_TENANTS_UPDATED = 'database_tenants_updated';
export const DATABASE_TOMBSTONED = 'database_tombstoned';
-export const ACTIVITY_TYPES = [
- LOGIN_ATTEMPT,
- RESOURCE_TAGGING,
- RESOURCE_UNTAGGING,
- API_KEY_GENERATION,
- SAVING_SUMA_SETTINGS,
- CHANGING_SUMA_SETTINGS,
- CLEARING_SUMA_SETTINGS,
- USER_CREATION,
- USER_MODIFICATION,
- USER_DELETION,
- PROFILE_UPDATE,
- CLUSTER_CHECKS_EXECUTION_REQUEST,
- ACTIVITY_LOG_SETTINGS_UPDATE,
- // Host events
- HEARTBEAT_FAILED,
- HEARTBEAT_SUCCEEDED,
- HOST_CHECKS_HEALTH_CHANGED,
- HOST_CHECKS_SELECTED,
- HOST_DEREGISTERED,
- HOST_DEREGISTRATION_REQUESTED,
- HOST_DETAILS_UPDATED,
- HOST_HEALTH_CHANGED,
- HOST_REGISTERED,
- HOST_RESTORED,
- HOST_ROLLED_UP,
- HOST_ROLL_UP_REQUESTED,
- HOST_SAPTUNE_HEALTH_CHANGED,
- HOST_TOMBSTONED,
- PROVIDER_UPDATED,
- SAPTUNE_STATUS_UPDATED,
- SLES_SUBSCRIPTIONS_UPDATED,
- SOFTWARE_UPDATES_DISCOVERY_CLEARED,
- SOFTWARE_UPDATES_DISCOVERY_REQUESTED,
- SOFTWARE_UPDATES_HEALTH_CHANGED,
- // Cluster events
- CHECKS_SELECTED,
- CLUSTER_CHECKS_HEALTH_CHANGED,
- CLUSTER_DEREGISTERED,
- CLUSTER_DETAILS_UPDATED,
- CLUSTER_DISCOVERED_HEALTH_CHANGED,
- CLUSTER_HEALTH_CHANGED,
- CLUSTER_REGISTERED,
- CLUSTER_RESTORED,
- CLUSTER_ROLLED_UP,
- CLUSTER_ROLL_UP_REQUESTED,
- CLUSTER_TOMBSTONED,
- HOST_ADDED_TO_CLUSTER,
- HOST_REMOVED_FROM_CLUSTER,
- // SAP System events
- APPLICATION_INSTANCE_DEREGISTERED,
- APPLICATION_INSTANCE_HEALTH_CHANGED,
- APPLICATION_INSTANCE_MARKED_ABSENT,
- APPLICATION_INSTANCE_MARKED_PRESENT,
- APPLICATION_INSTANCE_MOVED,
- APPLICATION_INSTANCE_REGISTERED,
- SAP_SYSTEM_DATABASE_HEALTH_CHANGED,
- SAP_SYSTEM_DEREGISTERED,
- SAP_SYSTEM_HEALTH_CHANGED,
- SAP_SYSTEM_REGISTERED,
- SAP_SYSTEM_RESTORED,
- SAP_SYSTEM_ROLLED_UP,
- SAP_SYSTEM_ROLL_UP_REQUESTED,
- SAP_SYSTEM_TOMBSTONED,
- SAP_SYSTEM_UPDATED,
- // Database events
- DATABASE_DEREGISTERED,
- DATABASE_HEALTH_CHANGED,
- DATABASE_INSTANCE_DEREGISTERED,
- DATABASE_INSTANCE_HEALTH_CHANGED,
- DATABASE_INSTANCE_MARKED_ABSENT,
- DATABASE_INSTANCE_MARKED_PRESENT,
- DATABASE_INSTANCE_REGISTERED,
- DATABASE_INSTANCE_SYSTEM_REPLICATION_CHANGED,
- DATABASE_REGISTERED,
- DATABASE_RESTORED,
- DATABASE_ROLLED_UP,
- DATABASE_ROLL_UP_REQUESTED,
- DATABASE_TENANTS_UPDATED,
- DATABASE_TOMBSTONED,
-];
-
const sumaSettingsResourceType = (_entry) => 'SUMA Settings';
const userResourceType = (_entry) => 'User';
const clusterResourceType = (_entry) => 'Cluster';
@@ -195,12 +117,15 @@ const taggingResourceType = (entry) =>
sap_system: sapSystemResourceType(entry),
})[entry.metadata?.resource_type] ?? 'Unable to determine resource type';
+const userManagement = ['all:all', 'all:users'];
+
export const ACTIVITY_TYPES_CONFIG = {
[LOGIN_ATTEMPT]: {
label: 'Login Attempt',
message: ({ metadata }) =>
metadata?.reason ? 'Login failed' : 'User logged in',
resource: (_entry) => 'Application',
+ allowedTo: userManagement,
},
[RESOURCE_TAGGING]: {
label: 'Tag Added',
@@ -238,21 +163,25 @@ export const ACTIVITY_TYPES_CONFIG = {
label: 'User Created',
message: (_entry) => `User was created`,
resource: userResourceType,
+ allowedTo: userManagement,
},
[USER_MODIFICATION]: {
label: 'User Modified',
message: (_entry) => `User was modified`,
resource: userResourceType,
+ allowedTo: userManagement,
},
[USER_DELETION]: {
label: 'User Deleted',
message: (_entry) => `User was deleted`,
resource: userResourceType,
+ allowedTo: userManagement,
},
[PROFILE_UPDATE]: {
label: 'Profile Updated',
message: (_entry) => `User modified profile`,
resource: (_entry) => 'Profile',
+ allowedTo: userManagement,
},
[CLUSTER_CHECKS_EXECUTION_REQUEST]: {
label: 'Checks Execution Requested',
@@ -580,6 +509,11 @@ export const ACTIVITY_TYPES_CONFIG = {
},
};
+export const ACTIVITY_TYPES = pipe(
+ entries,
+ map(([key, _value]) => key)
+)(ACTIVITY_TYPES_CONFIG);
+
const activityTypeConfig = ({ type }) => ACTIVITY_TYPES_CONFIG[type];
export const toLabel = (entry) =>
@@ -591,6 +525,18 @@ export const toMessage = (entry) =>
export const toResource = (entry) =>
activityTypeConfig(entry)?.resource(entry) ?? 'Unrecognized resource';
+export const allowedActivities = (abilities) =>
+ pipe(
+ entries,
+ filter(
+ ([_key, value]) =>
+ !has(value, 'allowedTo') ||
+ pipe(get('allowedTo'), (allowedTo) =>
+ isPermitted(abilities, allowedTo)
+ )(value)
+ )
+ )(ACTIVITY_TYPES_CONFIG);
+
export const LEVEL_DEBUG = 'debug';
export const LEVEL_INFO = 'info';
export const LEVEL_WARNING = 'warning';
diff --git a/assets/js/lib/model/activityLog.test.js b/assets/js/lib/model/activityLog.test.js
new file mode 100644
index 0000000000..129cd9acf5
--- /dev/null
+++ b/assets/js/lib/model/activityLog.test.js
@@ -0,0 +1,49 @@
+import { abilityFactory } from '@lib/test-utils/factories/users';
+import { difference } from 'lodash';
+import {
+ ACTIVITY_TYPES,
+ allowedActivities,
+ LOGIN_ATTEMPT,
+ PROFILE_UPDATE,
+ USER_CREATION,
+ USER_DELETION,
+ USER_MODIFICATION,
+} from './activityLog';
+
+const nonUserManagementActivities = difference(ACTIVITY_TYPES, [
+ LOGIN_ATTEMPT,
+ USER_CREATION,
+ USER_MODIFICATION,
+ USER_DELETION,
+ PROFILE_UPDATE,
+]);
+
+describe('activityLog', () => {
+ it.each`
+ userAbilities | hasUserMgmtActivities
+ ${[]} | ${false}
+ ${[['all', 'all']]} | ${true}
+ ${[['all', 'users']]} | ${true}
+ ${[['all', 'all'], ['all', 'users']]} | ${true}
+ ${[['all', 'all'], ['foo', 'bar']]} | ${true}
+ ${[['all', 'users'], ['bar', 'baz']]} | ${true}
+ ${[['baz', 'qux'], ['bar', 'baz']]} | ${false}
+ ${[['baz', 'qux']]} | ${false}
+ ${[['qux', 'ber']]} | ${false}
+ `(
+ 'should return relevant activities for the given user abilities',
+ ({ userAbilities, hasUserMgmtActivities }) => {
+ const abilities = userAbilities.map(([name, resource]) =>
+ abilityFactory.build({ name, resource })
+ );
+
+ const relevantActivities = allowedActivities(abilities).map(
+ ([key, _value]) => key
+ );
+
+ hasUserMgmtActivities
+ ? expect(relevantActivities).toEqual(ACTIVITY_TYPES)
+ : expect(relevantActivities).toEqual(nonUserManagementActivities);
+ }
+ );
+});
diff --git a/assets/js/lib/model/users.js b/assets/js/lib/model/users.js
index 209825c038..cbe7686fb8 100644
--- a/assets/js/lib/model/users.js
+++ b/assets/js/lib/model/users.js
@@ -3,3 +3,8 @@ import { getFromConfig } from '@lib/config';
const TRENTO_ADMIN_USERNAME = getFromConfig('adminUsername') || 'admin';
export const isAdmin = (user) => user.username === TRENTO_ADMIN_USERNAME;
+
+export const isPermitted = (userAbilities, permittedFor) =>
+ userAbilities
+ .map(({ name, resource }) => `${name}:${resource}`)
+ .some((ability) => permittedFor.includes(ability));
diff --git a/assets/js/lib/model/users.test.js b/assets/js/lib/model/users.test.js
index 6fd5328dc5..9e41e36b5f 100644
--- a/assets/js/lib/model/users.test.js
+++ b/assets/js/lib/model/users.test.js
@@ -1,6 +1,10 @@
-import { adminUser, userFactory } from '@lib/test-utils/factories/users';
+import {
+ adminUser,
+ userFactory,
+ abilityFactory,
+} from '@lib/test-utils/factories/users';
-import { isAdmin } from './users';
+import { isPermitted, isAdmin } from './users';
describe('users', () => {
it('should check if a user is admin', () => {
@@ -10,4 +14,24 @@ describe('users', () => {
const user = userFactory.build({ username: 'other' });
expect(isAdmin(user)).toBe(false);
});
+
+ it.each`
+ permittedFor | expectedIsPermitted
+ ${[]} | ${false}
+ ${['foo:bar']} | ${true}
+ ${['baz:qux', 'bar:baz']} | ${true}
+ ${['baz:qux']} | ${false}
+ ${['qux:ber']} | ${false}
+ `(
+ 'should check if a user has permissions',
+ ({ permittedFor, expectedIsPermitted }) => {
+ const abilities = [
+ abilityFactory.build({ name: 'foo', resource: 'bar' }),
+ abilityFactory.build({ name: 'bar', resource: 'baz' }),
+ ];
+
+ expect(isPermitted(abilities, permittedFor)).toBe(expectedIsPermitted);
+ expect(isPermitted([], permittedFor)).toBe(false);
+ }
+ );
});
diff --git a/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx b/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
index c581b9faed..7ed8b592e5 100644
--- a/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
+++ b/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
@@ -1,48 +1,55 @@
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+
+import { map, pipe } from 'lodash/fp';
+
+import { getActivityLog } from '@lib/api/activityLogs';
+import { allowedActivities } from '@lib/model/activityLog';
+
+import { getUserProfile } from '@state/selectors/user';
import PageHeader from '@common/PageHeader';
import ActivityLogOverview from '@common/ActivityLogOverview';
import ComposedFilter from '@common/ComposedFilter';
-import { getActivityLog } from '@lib/api/activityLogs';
-import { ACTIVITY_TYPES_CONFIG } from '@lib/model/activityLog';
import {
filterValueToSearchParams,
searchParamsToAPIParams,
searchParamsToFilterValue,
} from './searchParams';
-const filters = [
- {
- key: 'type',
- type: 'select',
- title: 'Resource type',
- options: Object.entries(ACTIVITY_TYPES_CONFIG).map(([key, value]) => [
- key,
- value.label,
- ]),
- },
- {
- key: 'to_date',
- title: 'newer than',
- type: 'date',
- prefilled: true,
- },
- {
- key: 'from_date',
- title: 'older than',
- type: 'date',
- prefilled: true,
- },
-];
-
function ActivityLogPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [activityLog, setActivityLog] = useState([]);
const [isLoading, setLoading] = useState(true);
const [activityLogDetailModalOpen, setActivityLogDetailModalOpen] =
useState(false);
+ const { abilities } = useSelector(getUserProfile);
+
+ const filters = [
+ {
+ key: 'type',
+ type: 'select',
+ title: 'Resource type',
+ options: pipe(
+ allowedActivities,
+ map(([key, value]) => [key, value.label])
+ )(abilities),
+ },
+ {
+ key: 'to_date',
+ title: 'newer than',
+ type: 'date',
+ prefilled: true,
+ },
+ {
+ key: 'from_date',
+ title: 'older than',
+ type: 'date',
+ prefilled: true,
+ },
+ ];
const fetchActivityLog = () => {
setLoading(true);
diff --git a/assets/js/pages/ActivityLogPage/ActivityLogPage.test.jsx b/assets/js/pages/ActivityLogPage/ActivityLogPage.test.jsx
index 9781a67f57..60b1c1a849 100644
--- a/assets/js/pages/ActivityLogPage/ActivityLogPage.test.jsx
+++ b/assets/js/pages/ActivityLogPage/ActivityLogPage.test.jsx
@@ -3,7 +3,7 @@ import { screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import MockAdapter from 'axios-mock-adapter';
-import { renderWithRouter } from '@lib/test-utils';
+import { renderWithRouter, withDefaultState } from '@lib/test-utils';
import { networkClient } from '@lib/network';
import { activityLogEntryFactory } from '@lib/test-utils/factories/activityLog';
@@ -15,7 +15,8 @@ const axiosMock = new MockAdapter(networkClient);
describe('ActivityLogPage', () => {
it('should render table without data', async () => {
axiosMock.onGet('/api/v1/activity_log').reply(200, { data: [] });
- await act(async () => renderWithRouter());
+ const [StatefulActivityLogPage] = withDefaultState();
+ await act(async () => renderWithRouter(StatefulActivityLogPage));
expect(screen.getByText('No data available')).toBeVisible();
});
@@ -38,8 +39,8 @@ describe('ActivityLogPage', () => {
axiosMock
.onGet('/api/v1/activity_log')
.reply(responseStatus, responseBody);
-
- await act(() => renderWithRouter());
+ const [StatefulActivityLogPage] = withDefaultState();
+ await act(() => renderWithRouter(StatefulActivityLogPage));
expect(screen.getByText('No data available')).toBeVisible();
}
@@ -50,8 +51,10 @@ describe('ActivityLogPage', () => {
.onGet('/api/v1/activity_log')
.reply(200, { data: activityLogEntryFactory.buildList(5) });
+ const [StatefulActivityLogPage] = withDefaultState();
+
const { container } = await act(() =>
- renderWithRouter()
+ renderWithRouter(StatefulActivityLogPage)
);
expect(container.querySelectorAll('tbody > tr')).toHaveLength(5);