From ad3735e346d29ec3b5e3aeaced65283890c4979c Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 11 Dec 2023 23:46:09 +0100 Subject: [PATCH 01/22] Use null instead of undefined --- .../lib/ability/caslAbility.ts | 2 +- .../lib/authorization/caslRules.ts | 23 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/management-system-v2/lib/ability/caslAbility.ts b/src/management-system-v2/lib/ability/caslAbility.ts index 1b8e9a0c0..2988981e4 100644 --- a/src/management-system-v2/lib/ability/caslAbility.ts +++ b/src/management-system-v2/lib/ability/caslAbility.ts @@ -65,7 +65,7 @@ type ConditionOperator = keyof typeof conditions; type ConditionsObject = { conditions: { [path: string]: { - [C in ConditionOperator]?: Parameters<(typeof conditions)[C]>[0]; + [C in ConditionOperator]?: Parameters<(typeof conditions)[C]>[0] | null; }; }; wildcardOperator?: 'or' | 'and'; diff --git a/src/management-system-v2/lib/authorization/caslRules.ts b/src/management-system-v2/lib/authorization/caslRules.ts index e6939285f..1c5f37ac2 100644 --- a/src/management-system-v2/lib/authorization/caslRules.ts +++ b/src/management-system-v2/lib/authorization/caslRules.ts @@ -218,7 +218,7 @@ async function rulesForSharedResources(ability: CaslAbility, userId: string) { conditions: { conditions: { id: { $eq: share.resourceId }, - $: { $not_expired_value: share.expiredAt ?? undefined }, + $: { $not_expired_value: share.expiredAt ?? null }, }, }, }); @@ -240,7 +240,7 @@ function rulesForShares(resource: ResourceType, userId: string, expiration: stri conditions: { resourceOwner: { $eq: userId }, resourceType: { $eq_string_case_insensitive: resource }, - $: { $not_expired_value: expiration ?? undefined }, + $: { $not_expired_value: expiration ?? null }, }, conditionsOperator: 'and', }, @@ -253,7 +253,7 @@ function rulesForShares(resource: ResourceType, userId: string, expiration: stri conditions: { sharedBy: { $eq: userId }, resourceType: { $eq_string_case_insensitive: resource }, - $: { $not_expired_value: expiration ?? undefined }, + $: { $not_expired_value: expiration ?? null }, }, conditionsOperator: 'and', }, @@ -285,11 +285,12 @@ function rulesForAlteringShares(ability: CaslAbility) { } type ReturnOfPromise = Fn extends (...args: any) => Promise ? Return : never; -export type PackedRulesForUser = ReturnOfPromise; +export type PackedRulesForUser = ReturnOfPromise; -export async function rulesForUser(userId: string) { +/** If possible don't use this function directly, use rulesForUser which caches the rules */ +export async function computeRulesForUser(userId: string) { const roles = getAppliedRolesForUser(userId); - let firstExpiration: undefined | Date; + let firstExpiration: null | Date = null; const translatedRules: AbilityRule[] = []; @@ -321,7 +322,7 @@ export async function rulesForUser(userId: string) { action: 'manage-roles', conditions: { conditions: { - $: { $not_expired_value: role.expiration ?? undefined }, + $: { $not_expired_value: role.expiration ?? null }, }, }, }); @@ -337,14 +338,12 @@ export async function rulesForUser(userId: string) { conditions: { conditions: { resourceType: { $eq_string_case_insensitive: resource }, - $: { $not_expired_value: role.expiration ?? undefined }, + $: { $not_expired_value: role.expiration ?? null }, }, }, }); const ownershipConditions = - needOwnership.has(resource) && !actionsSet.has('admin') - ? { owner: { $eq: userId } } - : undefined; + needOwnership.has(resource) && !actionsSet.has('admin') ? { owner: { $eq: userId } } : null; translatedRules.push({ subject: resource, @@ -352,7 +351,7 @@ export async function rulesForUser(userId: string) { conditions: { conditions: { ...ownershipConditions, - $: { $not_expired_value: role.expiration ?? undefined }, + $: { $not_expired_value: role.expiration ?? null }, }, }, }); From 8975a4771b358243ef6f53cca3dfd30080937ce2 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 11 Dec 2023 23:51:51 +0100 Subject: [PATCH 02/22] Changed ability and csrf setup --- .../app/(dashboard)/layout.tsx | 15 +++++++++++-- src/management-system-v2/components/app.tsx | 5 ++--- .../components/auth-can.tsx | 22 ------------------- src/management-system-v2/lib/abilityStore.ts | 9 ++++++++ .../lib/csrfTokenStore.ts | 18 ++++++++++++++- 5 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/layout.tsx b/src/management-system-v2/app/(dashboard)/layout.tsx index c2bcd9ed7..4ed14a6b5 100644 --- a/src/management-system-v2/app/(dashboard)/layout.tsx +++ b/src/management-system-v2/app/(dashboard)/layout.tsx @@ -1,8 +1,19 @@ import { FC, PropsWithChildren } from 'react'; import Layout from '@/components/layout'; +import { getUserRules } from '@/lib/authorization/authorization'; +import { getCurrentUser } from '@/components/auth'; +import { SetAbility } from '@/lib/abilityStore'; -const DashboardLayout: FC = ({ children }) => { - return {children}; +const DashboardLayout: FC = async ({ children }) => { + const { session } = await getCurrentUser(); + const userRules = await getUserRules(session?.user.id ?? ''); + + return ( + <> + + {children} + + ); }; export default DashboardLayout; diff --git a/src/management-system-v2/components/app.tsx b/src/management-system-v2/components/app.tsx index ebb09c16e..1940a94d6 100644 --- a/src/management-system-v2/components/app.tsx +++ b/src/management-system-v2/components/app.tsx @@ -6,16 +6,15 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { App as AntDesignApp } from 'antd'; import { SessionProvider } from 'next-auth/react'; -import { FetchAbility } from './auth-can'; - +import { SetCsrfToken } from '@/lib/csrfTokenStore'; const queryClient = new QueryClient(); const App: FC = ({ children }) => { return ( - + {children} diff --git a/src/management-system-v2/components/auth-can.tsx b/src/management-system-v2/components/auth-can.tsx index b709eda4d..d14bb2e5b 100644 --- a/src/management-system-v2/components/auth-can.tsx +++ b/src/management-system-v2/components/auth-can.tsx @@ -19,28 +19,6 @@ export type AuthCanProps = { loading?: ReactElement; }; -const API_URL = process.env.API_URL; - -export const FetchAbility = () => { - const setCsrfToken = useCsrfTokenStore((store) => store.setCsrfToken); - const { status, data } = useSession(); - const setAbility = useAbilityStore((store) => store.setAbility); - - useEffect(() => { - if (status === 'authenticated') { - setCsrfToken(data.csrfToken); - fetch(`${API_URL}/ability`, { - credentials: 'include', - headers: { 'csrf-token': data.csrfToken }, - }) - .then((r) => r.json()) - .then(({ rules }: { rules: PackRule[] }) => setAbility(rules)); - } - }, [status, setAbility, data, setCsrfToken]); - - return <>; -}; - // TODO: Weil client side werden evtl. sensible Daten an den Client geschickt. // Auf server side ändern und eigene component für client side die aber nur für // buttons etc. benutzt werden sollte diff --git a/src/management-system-v2/lib/abilityStore.ts b/src/management-system-v2/lib/abilityStore.ts index 8d9c38368..fcd999421 100644 --- a/src/management-system-v2/lib/abilityStore.ts +++ b/src/management-system-v2/lib/abilityStore.ts @@ -4,6 +4,7 @@ import { create } from 'zustand'; import { PackRule, packRules } from '@casl/ability/extra'; import Ability from './ability/abilityHelper'; import { AbilityRule } from './ability/caslAbility'; +import { useEffect } from 'react'; type AbilityStoreType = { ability: Ability; @@ -11,6 +12,14 @@ type AbilityStoreType = { setAbility: (rules: PackRule[]) => void; }; +export const SetAbility = ({ rules }: { rules: PackRule[] }) => { + useEffect(() => { + useAbilityStore.getState().setAbility(rules); + }, [rules]); + + return null; +}; + export const useAbilityStore = create((set) => ({ ability: new Ability(packRules([{ action: 'admin', subject: 'All' }] as AbilityRule[])), abilityFetched: false, diff --git a/src/management-system-v2/lib/csrfTokenStore.ts b/src/management-system-v2/lib/csrfTokenStore.ts index 5d9c34f38..98619d2f8 100644 --- a/src/management-system-v2/lib/csrfTokenStore.ts +++ b/src/management-system-v2/lib/csrfTokenStore.ts @@ -1,5 +1,7 @@ 'use client'; +import { useSession } from 'next-auth/react'; +import { useEffect } from 'react'; import { create } from 'zustand'; type CsrfTokenStore = { @@ -7,7 +9,21 @@ type CsrfTokenStore = { setCsrfToken: (csrfToken: string) => void; }; +/* Even though the csrf token is accessible through useSession, + * functions outside of react need to be able to access it*/ +export const SetCsrfToken = () => { + const session = useSession(); + + useEffect(() => { + if (session.status === 'authenticated') { + useCsrfTokenStore.getState().setCsrfToken(session.data.csrfToken); + } + }, [session]); + + return null; +}; + export const useCsrfTokenStore = create((set) => ({ - csrfToken: 'hola', + csrfToken: '', setCsrfToken: (csrfToken) => set({ csrfToken }), })); From 58a6b258bea1172a97a486710dc123ca738fb463 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 11 Dec 2023 23:52:45 +0100 Subject: [PATCH 03/22] Changed redis for lru-cache --- .../lib/authorization/authorization.ts | 94 +++++++------------ src/management-system-v2/package.json | 2 +- yarn.lock | 10 +- 3 files changed, 42 insertions(+), 64 deletions(-) diff --git a/src/management-system-v2/lib/authorization/authorization.ts b/src/management-system-v2/lib/authorization/authorization.ts index 318466030..b33b6f2c3 100644 --- a/src/management-system-v2/lib/authorization/authorization.ts +++ b/src/management-system-v2/lib/authorization/authorization.ts @@ -1,79 +1,57 @@ -import createConfig, { config as _config } from '../data/legacy/iam/config'; -import { PackedRulesForUser, rulesForUser } from './caslRules'; -import logger from '../data/legacy/logging'; - -import Redis from 'ioredis'; +import { PackedRulesForUser, computeRulesForUser } from './caslRules'; import Ability from '../ability/abilityHelper'; - -const config = _config as any; - -let rulesCache: Redis; - -export function initialiazeRulesCache(msConfig: typeof config) { - if (msConfig.useAuthorization) - rulesCache = new Redis(config.redisPort || 6379, config.redisHost || 'localhost', { - password: config.redisPassword || 'password', - db: 1, - }); +import { LRUCache } from 'lru-cache'; + +type PackedRules = PackedRulesForUser['rules']; + +const rulesCache = + // @ts-ignore + (global.rulesCache as LRUCache) || + // @ts-ignore + (global.rulesCache = new LRUCache({ + max: 500, + allowStale: false, + updateAgeOnGet: true, + updateAgeOnHas: true, + })); + +export function deleteCachedRulesForUser(userId: string) { + rulesCache.delete(userId); } -export async function deleteRulesForUsers(userId: string) { - await rulesCache.del(userId); +export function rulesCacheDeleteAll() { + rulesCache.clear(); } -export async function abilityCacheDeleteAll() { - await rulesCache.flushdb(); -} - -export async function setRulesForUser( +export function cacheRulesForUser( userId: string, - rules: PackedRulesForUser['rules'], + rules: PackedRules, expiration?: PackedRulesForUser['expiration'], ) { if (expiration) { - const secondsToExpiration = Math.round((+expiration - Date.now()) / 1000); - await rulesCache.set(userId, JSON.stringify(rules), 'EX', secondsToExpiration); + // TODO ttl is reset on get, it isn't a security issue since abilities check themselves if they're expired + // but it still should be fixed + const ttl = Math.round(+expiration - Date.now()); + rulesCache.set(userId, rules, { ttl }); } else { - await rulesCache.set(userId, JSON.stringify(rules)); - } -} - -export async function getRulesForUser( - userId: string, -): Promise { - try { - const rulesJson = await rulesCache.get(userId); - if (rulesJson === null) return undefined; - return JSON.parse(rulesJson); - } catch (e) { - return undefined; + rulesCache.set(userId, rules); } } -export async function getAbilityForUser(userId: string) { - let userRules = await getRulesForUser(userId); +export async function getUserRules(userId: string) { + let userRules = rulesCache.get(userId); if (userRules === undefined) { - const { rules, expiration } = await rulesForUser(userId); - setRulesForUser(userId, rules, expiration); + const { rules, expiration } = await computeRulesForUser(userId); + cacheRulesForUser(userId, rules, expiration); userRules = rules; } - const userAbility = new Ability(userRules); - return userAbility; + return userRules; } -let msConfig; - -try { - const c = require( - `../../../management-system/src/backend/server/environment-configurations/${process.env.NODE_ENV}/config_iam.json`, - ); - msConfig = createConfig(c); -} catch (e) { - console.log('FAILED', e); - msConfig = createConfig(); - logger.info('Started MS without Authentication and Authorization.'); +export async function getAbilityForUser(userId: string) { + const userRules = await getUserRules(userId); + const userAbility = new Ability(userRules); + return userAbility; } - -initialiazeRulesCache(msConfig); diff --git a/src/management-system-v2/package.json b/src/management-system-v2/package.json index 7e5f6c901..7ee307d4e 100644 --- a/src/management-system-v2/package.json +++ b/src/management-system-v2/package.json @@ -49,7 +49,7 @@ "bcryptjs": "^2.4.3", "js-md5": "^0.7.3", "yup": "^0.32.9", - "ioredis": "^5.0.1" + "lru-cache": "^10.1.0" }, "devDependencies": { "@tanstack/eslint-plugin-query": "4.36.0", diff --git a/yarn.lock b/yarn.lock index 759e19243..a758fdb2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14064,6 +14064,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.1.0, "lru-cache@^9.1.1 || ^10.0.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== + lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -14086,11 +14091,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" - integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== - machine-uuid@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/machine-uuid/-/machine-uuid-1.2.0.tgz#810ce7f8b5f59535102c947cf9ef1b4241779c18" From d9666cea0cca32857ec6f38b15a657345b1c0dc5 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 11:12:39 +0100 Subject: [PATCH 04/22] Fix: Admin role was being added twice --- src/management-system-v2/lib/authorization/rolesHelper.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/management-system-v2/lib/authorization/rolesHelper.ts b/src/management-system-v2/lib/authorization/rolesHelper.ts index 706e5f9af..523e01fbf 100644 --- a/src/management-system-v2/lib/authorization/rolesHelper.ts +++ b/src/management-system-v2/lib/authorization/rolesHelper.ts @@ -18,12 +18,6 @@ export function getAppliedRolesForUser(userId: string): Role[] { const userRoles: Role[] = []; - const adminRole = Object.values(roleMetaObjects).find( - (role: any) => role.default && role.name === '@admin', - ) as any; - if (adminRole.members.map((member: any) => member.userId).includes(userId)) - userRoles.push(adminRole); - userRoles.push( Object.values(roleMetaObjects).find( (role: any) => role.default && role.name === '@everyone', From a138d7e0366624bd77da6b0d21523f72d5673a60 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 11:27:37 +0100 Subject: [PATCH 05/22] Used .global for stores and moved init functions --- .../lib/data/legacy/iam/role-mappings.js | 36 +++++++------- .../lib/data/legacy/iam/roles.js | 49 +++++++++---------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js index 49ca87a99..0f86187be 100644 --- a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js +++ b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js @@ -3,7 +3,24 @@ import store from '../store.js'; import { roleMetaObjects } from './roles.js'; /** @type {any} - object containing all role mappings */ -export let roleMappingsMetaObjects = {}; +export let roleMappingsMetaObjects = + global.roleMappingsMetaObjects || (global.roleMappingsMetaObjects = {}); + +/** + * initializes the role mappings meta information objects + */ +export function init() { + roleMappingsMetaObjects = {}; + + // get role mappings that were persistently stored + const storedRoleMappings = store.get('roleMappings'); + + // set role mappings store + store.set('roleMappings', storedRoleMappings); + + roleMappingsMetaObjects.users = storedRoleMappings.roleMappings.users; +} +init(); /** * Returns all role mappings in form of an array @@ -131,20 +148,3 @@ export async function deleteRoleMapping(userId, roleId) { store.update('roles', roleId, roleMetaObjects[roleId]); } } - -/** - * initializes the role mappings meta information objects - */ -export async function init() { - roleMappingsMetaObjects = {}; - - // get role mappings that were persistently stored - const storedRoleMappings = store.get('roleMappings'); - - // set role mappings store - store.set('roleMappings', storedRoleMappings); - - roleMappingsMetaObjects.users = storedRoleMappings.roleMappings.users; -} - -init(); diff --git a/src/management-system-v2/lib/data/legacy/iam/roles.js b/src/management-system-v2/lib/data/legacy/iam/roles.js index 12362d1ef..fb793456a 100644 --- a/src/management-system-v2/lib/data/legacy/iam/roles.js +++ b/src/management-system-v2/lib/data/legacy/iam/roles.js @@ -5,7 +5,30 @@ import { mergeIntoObject } from '../../../helpers/javascriptHelpers'; import { roleMappingsMetaObjects } from './role-mappings.js'; /** @type {any} - object containing all roles */ -export let roleMetaObjects = {}; +export let roleMetaObjects = global.roleMetaObjects || (global.roleMetaObjects = {}); + +/** + * initializes the roles meta information objects + */ +export function init() { + roleMetaObjects = {}; + + // get roles that were persistently stored + const storedRoles = store.get('roles'); + + // set roles store + store.set('roles', 'roles', storedRoles); + + // migrate roles + roleMigrations.forEach((role) => { + const index = storedRoles.findIndex((storedRole) => storedRole.name === role.name); + if (index < 0) addRole(role); + }); + + // set roles store cache for quick access + storedRoles.forEach((role) => (roleMetaObjects[role.id] = role)); +} +init(); /** * Returns all roles in form of an array @@ -131,27 +154,3 @@ export async function deleteRole(roleId) { }, }); } - -/** - * initializes the roles meta information objects - */ -export async function init() { - roleMetaObjects = {}; - - // get roles that were persistently stored - const storedRoles = store.get('roles'); - - // set roles store - store.set('roles', 'roles', storedRoles); - - // migrate roles - roleMigrations.forEach((role) => { - const index = storedRoles.findIndex((storedRole) => storedRole.name === role.name); - if (index < 0) addRole(role); - }); - - // set roles store cache for quick access - storedRoles.forEach((role) => (roleMetaObjects[role.id] = role)); -} - -init(); From 6874f8893a3e0e36ff00eef0b145849103b1be82 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:15:13 +0100 Subject: [PATCH 06/22] Bug fix: Authcan redirected when loading --- src/management-system-v2/components/auth-can.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/management-system-v2/components/auth-can.tsx b/src/management-system-v2/components/auth-can.tsx index d14bb2e5b..880639ee2 100644 --- a/src/management-system-v2/components/auth-can.tsx +++ b/src/management-system-v2/components/auth-can.tsx @@ -37,8 +37,10 @@ export const AuthCan: FC> = ({ const ability = useAbilityStore((store) => store.ability); const abilityFetched = useAbilityStore((store) => store.abilityFetched); + const loadingState = status === 'loading' || !abilityFetched; + const allow = useMemo(() => { - if (status !== 'authenticated' || !abilityFetched) return false; + if (status !== 'authenticated' || loadingState) return false; const resources = Array.isArray(resource) ? resource : [resource]; const actions = Array.isArray(action) ? action : [action]; @@ -50,16 +52,18 @@ export const AuthCan: FC> = ({ } return true; - }, [action, resource, ability, abilityFetched, status]); + }, [action, resource, ability, loadingState, status]); useEffect(() => { - if (abilityFetched && !allow && fallbackRedirect) router.push(fallbackRedirect); - }, [allow, fallbackRedirect, router, abilityFetched]); + if (!loadingState && !allow && fallbackRedirect) { + router.push(fallbackRedirect); + } + }, [allow, fallbackRedirect, router, loadingState]); if (!process.env.NEXT_PUBLIC_USE_AUTH) return children; if (status === 'unauthenticated' && notLoggedIn) return notLoggedIn; - if (status === 'loading' || !abilityFetched) return loadingAuth || null; + if (loadingState) return loadingAuth || null; if (allow) return children; return fallback || null; From b6c31dd70dfd1ac7b4613d20683c1d90f6f10dd6 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:17:41 +0100 Subject: [PATCH 07/22] Ability filter generic --- src/management-system-v2/lib/ability/abilityHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/lib/ability/abilityHelper.ts b/src/management-system-v2/lib/ability/abilityHelper.ts index 87d5b823f..11fdde306 100644 --- a/src/management-system-v2/lib/ability/abilityHelper.ts +++ b/src/management-system-v2/lib/ability/abilityHelper.ts @@ -25,10 +25,10 @@ export default class Ability { return true; } - filter(action: CanParams[0] | CanParams[0][], resource: ResourceType, array: any[]) { + filter(action: CanParams[0] | CanParams[0][], resource: ResourceType, array: T[]) { return array.filter((resourceInstance) => this.can(action, toCaslResource(resource, resourceInstance)), - ); + ) as T[]; } checkInputFields( From 2e4bab4fb6d13586b05c85314cd86805449bf813 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:18:58 +0100 Subject: [PATCH 08/22] Bug fix: Ability filter recognize null values --- src/management-system-v2/lib/ability/abilityHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/ability/abilityHelper.ts b/src/management-system-v2/lib/ability/abilityHelper.ts index 11fdde306..dacaa79a6 100644 --- a/src/management-system-v2/lib/ability/abilityHelper.ts +++ b/src/management-system-v2/lib/ability/abilityHelper.ts @@ -37,7 +37,7 @@ export default class Ability { input: any, prefix: string = '', ) { - if (typeof input !== 'object') return this.can(action, resourceObj, prefix); + if (typeof input !== 'object' || input === null) return this.can(action, resourceObj, prefix); for (const key of Object.keys(input)) { if ( From 215f0065b0cc38b4e91234eadf182d638d575918 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:19:42 +0100 Subject: [PATCH 09/22] Unauthorized error class --- src/management-system-v2/lib/ability/abilityHelper.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/management-system-v2/lib/ability/abilityHelper.ts b/src/management-system-v2/lib/ability/abilityHelper.ts index dacaa79a6..e4b263312 100644 --- a/src/management-system-v2/lib/ability/abilityHelper.ts +++ b/src/management-system-v2/lib/ability/abilityHelper.ts @@ -49,3 +49,10 @@ export default class Ability { return true; } } + +export class UnauthorizedError extends Error { + constructor(message: string | undefined) { + super(message ?? 'Unauthorized'); + this.name = 'UnauthorizedError'; + } +} From df81a6559a48f1d70e66cb817cb505b8f4fc5132 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:21:23 +0100 Subject: [PATCH 10/22] Added permissions check and changed init --- .../lib/data/legacy/iam/role-mappings.js | 46 +++++++--- .../lib/data/legacy/iam/roles.js | 91 ++++++++++++++----- 2 files changed, 104 insertions(+), 33 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js index 0f86187be..0b2ba7795 100644 --- a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js +++ b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js @@ -1,6 +1,11 @@ import { v4 } from 'uuid'; import store from '../store.js'; import { roleMetaObjects } from './roles.js'; +import { ApiData, ApiRequestBody } from '@/lib/fetch-data'; +import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; +import { toCaslResource } from '@/lib/ability/caslAbility'; + +let firstInit = !global.roleMappingsMetaObjects; /** @type {any} - object containing all role mappings */ export let roleMappingsMetaObjects = @@ -10,7 +15,7 @@ export let roleMappingsMetaObjects = * initializes the role mappings meta information objects */ export function init() { - roleMappingsMetaObjects = {}; + if (!firstInit) return; // get role mappings that were persistently stored const storedRoleMappings = store.get('roleMappings'); @@ -25,31 +30,39 @@ init(); /** * Returns all role mappings in form of an array * - * @returns {Array} - array containing all role mappings + * @param {Ability} ability + * @returns {ApiData<'/role-mappings','get'>} - array containing all role mappings */ -export function getRoleMappings() { - return Object.values(roleMappingsMetaObjects.users).flat(); +export function getRoleMappings(ability) { + const roleMappings = Object.values(roleMappingsMetaObjects.users).flat(); + + return ability.filter('view', 'RoleMapping', roleMappings); } /** * Returns a role mapping by user id * * @param {String} userId - the id of a user - * @returns {Array} - role mappings of a user + * @param {Ability} ability + * + * @returns {ApiData<'/role-mappings/users/{userId}','get'>} - role mappings of a user */ -export function getRoleMappingByUserId(userId) { - return roleMappingsMetaObjects.users[userId]; +export function getRoleMappingByUserId(userId, ability) { + const roleMappings = roleMappingsMetaObjects.users[userId]; + return ability.filter('view', 'RoleMapping', roleMappings); } // TODO: also check if user exists? /** * Adds a user role mapping * - * @param {Object} roleMapping - role mapping object containing userId & roleId - * @returns {Object} - new user role mapping + * @param {ApiRequestBody<'/role-mappings','post'>} roleMappings - role mapping object containing userId & roleId + * @param {Ability} ability */ -export async function addRoleMapping(roleMappings) { - roleMappings.forEach((roleMapping) => { +export async function addRoleMappings(roleMappings, ability) { + const allowedRoleMappings = ability.filter('create', 'RoleMapping', roleMappings); + + allowedRoleMappings.forEach((roleMapping) => { const { roleId, userId } = roleMapping; if (roleId && userId) { let role = roleMetaObjects[roleId]; @@ -111,9 +124,18 @@ export async function addRoleMapping(roleMappings) { * * @param {String} userId - id of user * @param {String} roleId - role mapping that has to be removed based on roleId + * @param {Ability} ability + * * @returns {Object} - new mapping object without removed element */ -export async function deleteRoleMapping(userId, roleId) { +export async function deleteRoleMapping(userId, roleId, ability) { + const roleMapping = roleMappingsMetaObjects.users[userId].find( + (roleMapping) => roleMapping.roleId === roleId, + ); + + if (!ability.can('delete', toCaslResource('RoleMapping', roleMapping))) + throw new UnauthorizedError(); + if (userId && roleId) { if (!roleMappingsMetaObjects.users[userId]) { throw new Error('Mapping not found'); diff --git a/src/management-system-v2/lib/data/legacy/iam/roles.js b/src/management-system-v2/lib/data/legacy/iam/roles.js index fb793456a..450e9d68e 100644 --- a/src/management-system-v2/lib/data/legacy/iam/roles.js +++ b/src/management-system-v2/lib/data/legacy/iam/roles.js @@ -3,6 +3,11 @@ import store from '../store.js'; import { roleMigrations } from './migrations/role-migrations.js'; import { mergeIntoObject } from '../../../helpers/javascriptHelpers'; import { roleMappingsMetaObjects } from './role-mappings.js'; +import { ApiData, ApiRequestBody } from '@/lib/fetch-data'; +import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; +import { toCaslResource } from '@/lib/ability/caslAbility'; + +let firstInit = !global.roleMetaObjects; /** @type {any} - object containing all roles */ export let roleMetaObjects = global.roleMetaObjects || (global.roleMetaObjects = {}); @@ -11,7 +16,7 @@ export let roleMetaObjects = global.roleMetaObjects || (global.roleMetaObjects = * initializes the roles meta information objects */ export function init() { - roleMetaObjects = {}; + if (!firstInit) return; // get roles that were persistently stored const storedRoles = store.get('roles'); @@ -33,29 +38,49 @@ init(); /** * Returns all roles in form of an array * - * @returns {Array} - array containing all roles + * @param {Ability} ability + * + * @returns {ApiData<'/roles','get'>} - array containing all roles */ -export function getRoles() { - return Object.values(roleMetaObjects); +export function getRoles(ability) { + const roles = Object.values(roleMetaObjects); + + return ability.filter('view', 'Process', roles); } /** * Returns a role based on role id * + * @throws {UnauthorizedError} + * * @param {String} roleId - the id of a role - * @returns {Object} - role object + * @param {Ability} ability + * + * @returns {ApiData<'/roles/{id}','get'> | undefined} - role object */ -export function getRoleById(roleId) { - return roleMetaObjects[roleId]; +export function getRoleById(roleId, ability) { + const role = roleMetaObjects[roleId]; + + if (role && !ability.can('view', toCaslResource('Role', role))) throw new UnauthorizedError(); + + return role; } /** * Adds a new role for the PROCEED MS * - * @param {Object} roleRepresentation - role representation - * @returns {Object} - newly created role + * @throws {UnauthorizedError} + * @throws {Error} + * + * @param {ApiRequestBody<'/roles','post'> } roleRepresentation - role representation + * @param {Ability} ability + * + * @returns {ApiData<'/roles/{id}','get'> } - role object */ -export async function addRole(roleRepresentation) { +export function addRole(roleRepresentation, ability) { + if (!ability.can('create', toCaslResource('Role', roleRepresentation))) + throw new UnauthorizedError(); + const { name, description, note, permissions, expiration } = roleRepresentation; const index = Object.values(roleMetaObjects).findIndex((role) => role.name === name); @@ -95,17 +120,31 @@ export async function addRole(roleRepresentation) { /** * Updates a role by id for the PROCEED MS * + * @throws {UnauthorizedError} + * @throws {Error} + * * @param {String} roleId - if of role - * @param {Object} roleRepresentation - role representation - * @returns {Object} - updated role + * @param {ApiRequestBody<'/roles/{id}','put'> } roleRepresentation - role representation + * @param {Ability} ability + * + * @returns {ApiData<'/roles/{id}','get'> } - updated role */ -export async function updateRole(roleId, roleRepresentation) { - if (!roleMetaObjects[roleId]) { - throw new Error('Role not found'); - } +export function updateRole(roleId, roleRepresentation, ability) { + const targetRole = roleMetaObjects[roleId]; + + if (!targetRole) throw new Error('Role not found'); + + // Casl isn't really built to check the value of input fields when updating, so we have to perform this two checks + if ( + !( + ability.checkInputFields(toCaslResource('Role', targetRole), 'update', targetRole) && + ability.can('create', toCaslResource('Role', roleRepresentation)) + ) + ) + throw new UnauthorizedError(); // merge and save at local cache - await mergeIntoObject(roleMetaObjects[roleId], roleRepresentation, true); + mergeIntoObject(roleMetaObjects[roleId], roleRepresentation, true); roleMetaObjects[roleId].lastEdited = new Date().toUTCString(); Object.keys(roleMetaObjects[roleId].permissions).forEach((key) => { @@ -122,12 +161,17 @@ export async function updateRole(roleId, roleRepresentation) { /** * Deletes a role from the PROCEED MS * + * @throws {UnauthorizedError} + * @throws {Error} + * * @param {String} roleId - the id of a role + * @param {Ability} ability */ -export async function deleteRole(roleId) { - if (!roleMetaObjects[roleId]) { - throw new Error('Role not found'); - } +export async function deleteRole(roleId, ability) { + const role = roleMetaObjects[roleId]; + if (!role) throw new Error('Role not found'); + + if (!ability.can('delete', toCaslResource('Role', role))) throw new UnauthorizedError(); // remove from local cache delete roleMetaObjects[roleId]; @@ -153,4 +197,9 @@ export async function deleteRole(roleId) { users: { ...roleMappingsMetaObjects.users }, }, }); + + console.log( + 'leftkeys', + Object.keys(roleMetaObjects).map((r) => roleMetaObjects[r].name), + ); } From e489f8c9841225e15541144706b93df5b799d96a Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:26:33 +0100 Subject: [PATCH 11/22] Server actions for role-mappings and roles --- .../lib/data/role-mappings.ts | 28 +++++++++++++++ src/management-system-v2/lib/data/roles.ts | 34 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/management-system-v2/lib/data/role-mappings.ts create mode 100644 src/management-system-v2/lib/data/roles.ts diff --git a/src/management-system-v2/lib/data/role-mappings.ts b/src/management-system-v2/lib/data/role-mappings.ts new file mode 100644 index 000000000..9057d9588 --- /dev/null +++ b/src/management-system-v2/lib/data/role-mappings.ts @@ -0,0 +1,28 @@ +'use server'; + +import { getCurrentUser } from '@/components/auth'; +import { + deleteRoleMapping as _deleteRoleMapping, + addRoleMappings as _addRoleMappings, +} from './legacy/iam/role-mappings'; + +export async function addRoleMappings(roleMappings: Parameters[0]) { + const { ability } = await getCurrentUser(); + + _addRoleMappings(roleMappings, ability); +} + +export async function deleteRoleMappings(roleMappings: { userId: string; roleId: string }[]) { + const errors: { roleId: string; error: Error }[] = []; + + const { ability } = await getCurrentUser(); + for (const { userId, roleId } of roleMappings) { + try { + _deleteRoleMapping(userId, roleId, ability); + } catch (error) { + errors.push({ roleId, error: error as Error }); + } + } + + return errors; +} diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts new file mode 100644 index 000000000..6b37b1d28 --- /dev/null +++ b/src/management-system-v2/lib/data/roles.ts @@ -0,0 +1,34 @@ +'use server'; + +import { getCurrentUser } from '@/components/auth'; +import { deleteRole, addRole as _addRole, updateRole as _updateRole } from './legacy/iam/roles.js'; +import { redirect } from 'next/navigation.js'; + +export async function deleteRoles(roleIds: string[]) { + const { ability } = await getCurrentUser(); + + const errors: { roleId: string; error: Error }[] = []; + + for (const roleId of roleIds) { + try { + deleteRole(roleId, ability); + } catch (error) { + errors.push({ roleId, error: error as Error }); + } + } + + return errors; +} + +export async function addRole(role: Parameters[0]) { + const { ability } = await getCurrentUser(); + + const newRole = _addRole(role, ability); + redirect(`/iam/roles/${newRole.id}`); +} + +export async function updateRole(roleId: string, updatedRole: Parameters[1]) { + const { ability } = await getCurrentUser(); + + _updateRole(roleId, updatedRole, ability); +} From 01155c76ee7fdd42d5f83ab7cbd73c0fe724c3fd Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:27:07 +0100 Subject: [PATCH 12/22] Switched components to rsc and to server actions --- .../(dashboard)/iam/roles/[roleId]/page.tsx | 58 ++++++- .../iam/roles/[roleId]/role-members.tsx | 65 ++++---- .../iam/roles/[roleId]/role-page.tsx | 70 -------- .../iam/roles/[roleId]/roleGeneralData.tsx | 38 ++--- .../iam/roles/[roleId]/rolePermissions.tsx | 148 ++++++++--------- .../(dashboard)/iam/roles/header-actions.tsx | 41 +---- .../app/(dashboard)/iam/roles/page.tsx | 20 ++- .../app/(dashboard)/iam/roles/role-page.tsx | 150 ++++++++---------- 8 files changed, 254 insertions(+), 336 deletions(-) delete mode 100644 src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx index 3f7a9233e..c8265b20e 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx @@ -1,5 +1,57 @@ -import Auth from '@/components/auth'; -import RolePage from './role-page'; +import Auth, { getCurrentUser } from '@/components/auth'; +import Content from '@/components/content'; +import { getRoleById } from '@/lib/data/legacy/iam/roles'; +import { Button, Card, Space, Tabs } from 'antd'; +import { LeftOutlined } from '@ant-design/icons'; +import Link from 'next/link'; +import { ComponentProps } from 'react'; +import RoleGeneralData from './roleGeneralData'; +import RolePermissions from './rolePermissions'; +import RoleMembers from './role-members'; + +type Items = ComponentProps['items']; + +const Page = async ({ params: { roleId } }: { params: { roleId: string } }) => { + const { ability } = await getCurrentUser(); + const role = getRoleById(roleId, ability); + + const items: Items = role + ? [ + { + key: 'generalData', + label: 'General Data', + children: , + }, + { key: 'permissions', label: 'Permissions', children: }, + { + key: 'members', + label: 'Manage Members', + children: , + }, + ] + : []; + + return ( + + + + + {role?.name} + + } + > +
+ + + +
+
+ ); +}; export default Auth( { @@ -7,5 +59,5 @@ export default Auth( resource: 'Role', fallbackRedirect: '/', }, - RolePage, + Page, ); diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx index 94f784e00..28e8369f6 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx @@ -2,35 +2,31 @@ import { FC, useMemo, useState } from 'react'; import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; -import { - ApiData, - useDeleteAsset, - useGetAsset, - useInvalidateAsset, - usePostAsset, -} from '@/lib/fetch-data'; +import { ApiData, useGetAsset } from '@/lib/fetch-data'; import UserList, { UserListProps } from '@/components/user-list'; import { Button, Modal, Tooltip } from 'antd'; import ConfirmationButton from '@/components/confirmation-button'; +import { addRoleMappings, deleteRoleMappings } from '@/lib/data/role-mappings'; +import { useRouter } from 'next/navigation'; type Role = ApiData<'/roles', 'get'>[number]; +type Users = ApiData<'/users', 'get'>; const AddUserModal: FC<{ role: Role; open: boolean; close: () => void }> = ({ role, open, close, }) => { - const { data: users, isLoading: isLoadingUsers } = useGetAsset('/users', {}); - const invalidateRole = useInvalidateAsset('/roles/{id}', { params: { path: { id: role.id } } }); - const { mutateAsync, isLoading: isLoadingMutation } = usePostAsset('/role-mappings', { - onSuccess: invalidateRole, - }); + const [loading, setLoading] = useState(false); + const { data: users, refetch: refetchUsers, isLoading: usersLoading } = useGetAsset('/users', {}); + const router = useRouter(); type AddUserParams = Parameters>; const addUsers = async (users: AddUserParams[2], clearIds?: AddUserParams[1]) => { if (clearIds) clearIds(); - await mutateAsync({ - body: users.map((user) => ({ + setLoading(true); + await addRoleMappings( + users.map((user) => ({ userId: user.id, roleId: role.id, email: user.email.value, @@ -38,8 +34,10 @@ const AddUserModal: FC<{ role: Role; open: boolean; close: () => void }> = ({ firstName: user.firstName.value, username: user.username.value, })), - parseAs: 'text', - }); + ); + setLoading(false); + refetchUsers(); + router.refresh(); }; const usersNotInRole = useMemo(() => { @@ -60,7 +58,7 @@ const AddUserModal: FC<{ role: Role; open: boolean; close: () => void }> = ({ > [ { dataIndex: 'id', @@ -88,34 +86,33 @@ const AddUserModal: FC<{ role: Role; open: boolean; close: () => void }> = ({ ); }; -const RoleMembers: FC<{ role: Role; isLoadingRole?: boolean }> = ({ role, isLoadingRole }) => { +const RoleMembers: FC<{ role: Role }> = ({ role }) => { const [addUserModalOpen, setAddUserModalOpen] = useState(false); + const [loading, setLoading] = useState(false); + const router = useRouter(); - const refetchRole = useInvalidateAsset('/roles/{id}', { params: { path: { id: role.id } } }); - const { mutateAsync: deleteUser, isLoading: isLoadingDelete } = useDeleteAsset( - '/role-mappings/users/{userId}/roles/{roleId}', - { onSuccess: refetchRole }, - ); - - async function deleteMembers(userIds: string[], clearIds?: () => void) { - if (clearIds) clearIds(); + async function deleteMembers(userIds: string[], clearIds: () => void) { + clearIds(); + setLoading(true); - await Promise.allSettled( - userIds.map((userId) => - deleteUser({ - parseAs: 'text', - params: { path: { roleId: role.id, userId: userId } }, - }), - ), + await deleteRoleMappings( + userIds.map((userId) => ({ + roleId: role.id, + userId: userId, + })), ); + + setLoading(false); + router.refresh(); } return ( <> setAddUserModalOpen(false)} /> + ({ ...member, id: member.userId }))} - loading={isLoadingDelete || isLoadingRole} + loading={loading} columns={(clearSelected, hoveredId, selectedRowKeys) => [ { dataIndex: 'id', diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx deleted file mode 100644 index a2cddaf2d..000000000 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import Content from '@/components/content'; -import { useGetAsset } from '@/lib/fetch-data'; -import { Button, Result, Skeleton, Space, Tabs } from 'antd'; -import { LeftOutlined } from '@ant-design/icons'; -import { ComponentProps } from 'react'; -import RoleGeneralData from './roleGeneralData'; -import { useRouter } from 'next/navigation'; -import RolePermissions from './rolePermissions'; -import RoleMembers from './role-members'; - -type Items = ComponentProps['items']; - -function RolePage({ params: { roleId } }: { params: { roleId: string } }) { - const router = useRouter(); - const { - data: role, - isLoading, - error, - } = useGetAsset('/roles/{id}', { - params: { path: { id: roleId } }, - }); - - const items: Items = role - ? [ - { - key: 'generalData', - label: 'General Data', - children: , - }, - { key: 'permissions', label: 'Permissions', children: }, - { - key: 'members', - label: 'Manage Members', - children: , - }, - ] - : []; - - if (error) - return ( - - ); - - return ( - - - {role?.name} - - } - > -
- - - -
-
- ); -} - -export default RolePage; diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx index fbd2cc131..ac0ba6dc4 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx @@ -1,26 +1,21 @@ 'use client'; import { toCaslResource } from '@/lib/ability/caslAbility'; -import { useGetAsset, usePutAsset } from '@/lib/fetch-data'; -import { Alert, App, Button, DatePicker, Form, Input, Spin } from 'antd'; +import { ApiData } from '@/lib/fetch-data'; +import { Alert, App, Button, DatePicker, Form, Input } from 'antd'; import { FC, useEffect, useState } from 'react'; import dayjs from 'dayjs'; import germanLocale from 'antd/es/date-picker/locale/de_DE'; import { useAbilityStore } from '@/lib/abilityStore'; +import { updateRole } from '@/lib/data/roles'; -const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => { +type Role = ApiData<'/roles/{id}', 'get'>; + +const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => { const { message } = App.useApp(); const ability = useAbilityStore((store) => store.ability); const [form] = Form.useForm(); - const { data, isLoading, error } = useGetAsset('/roles/{id}', { - params: { path: { id: roleId } }, - }); - - const { mutateAsync: updateRole, isLoading: putLoading } = usePutAsset('/roles/{id}', { - onError: () => message.open({ type: 'error', content: 'Something went wrong' }), - }); - const [submittable, setSubmittable] = useState(false); const values = Form.useWatch('name', form); @@ -35,9 +30,7 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => { ); }, [form, values]); - if (isLoading || error || !data) return ; - - const role = toCaslResource('Role', data); + const role = toCaslResource('Role', _role); async function submitChanges(values: Record) { if (typeof values.expirationDayJs === 'object') { @@ -46,15 +39,14 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => { } try { - await updateRole({ - params: { path: { id: roleId } }, - body: values, - }); - - // success message has to go here, or else the mutation will stop loading when the message - // disappears + await updateRole(role.id, values); message.open({ type: 'success', content: 'Role updated' }); - } catch (e) {} + } catch (e) { + let msg = 'Something went wrong'; + if (e instanceof Error && e.message) msg += `: ${e.message}`; + + message.open({ type: 'error', content: msg }); + } } return ( @@ -93,7 +85,7 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => { - diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx index 4f4026464..e1bd35eff 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx @@ -15,9 +15,10 @@ import { import { SaveOutlined, LoadingOutlined } from '@ant-design/icons'; import { ResourceActionType } from '@/lib/ability/caslAbility'; import { FC, useState } from 'react'; -import { ApiData, usePutAsset } from '@/lib/fetch-data'; +import { ApiData } from '@/lib/fetch-data'; import { switchChecked, switchDisabled, togglePermission } from './role-permissions-helper'; import { useAbilityStore } from '@/lib/abilityStore'; +import { updateRole as serverUpdateRole } from '@/lib/data/roles'; type PermissionCategory = { key: string; @@ -260,108 +261,87 @@ type Role = ApiData<'/roles', 'get'>[number]; const RolePermissions: FC<{ role: Role }> = ({ role }) => { const [permissions, setPermissions] = useState(role.permissions); + const [loading, setLoading] = useState(false); const ability = useAbilityStore((store) => store.ability); - const { mutateAsync, isLoading } = usePutAsset('/roles/{id}', { - /* onSuccess: () => message.open({ content: 'Role updated', type: 'success' }), - onError: () => message.open({ content: 'Something went wrong', type: 'error' }), */ - }); const { message } = App.useApp(); const [form] = Form.useForm(); async function updateRole() { + setLoading(true); try { - await mutateAsync({ - params: { path: { id: role.id } }, - body: { - description: role.description, - name: role.name, - permissions: permissions, - note: role.note, - default: role.default, - expiration: role.expiration, - }, + await serverUpdateRole(role.id, { + permissions, }); message.open({ content: 'Role updated', type: 'success' }); } catch (e) { message.open({ content: 'Something went wrong', type: 'error' }); } + setLoading(false); } return ( -
-
- {basePermissionOptions.map((permissionCategory) => ( - <> - - {permissionCategory.title} - - {permissionCategory.permissions.map((permission, idx) => ( - <> - - - {permission.title} - {permission.description} - - - + {basePermissionOptions.map((permissionCategory) => ( + <> + + {permissionCategory.title} + + {permissionCategory.permissions.map((permission, idx) => ( + <> + + + {permission.title} + {permission.description} + + + + setPermissions( + togglePermission( permissions, permissionCategory.resource, permission.permission, - ability, - ) - } - checked={switchChecked( - permissions, - permissionCategory.resource, - permission.permission, - )} - onChange={() => - setPermissions( - togglePermission( - permissions, - permissionCategory.resource, - permission.permission, - ), - ) - } - /> - - - {idx < permissionCategory.permissions.length - 1 && ( - - )} - - ))} -
- - ))} + ), + ) + } + /> +
+
+ {idx < permissionCategory.permissions.length - 1 && ( + + )} + + ))} +
+ + ))} - - } /> - ) : ( - - ) - } - onClick={() => !isLoading && form.submit()} - /> - - -
+ + } /> + ) : ( + + ) + } + onClick={() => !loading && form.submit()} + /> + + ); }; diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx index 8d9f82c61..2d6556232 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx @@ -1,13 +1,12 @@ 'use client'; -import { usePostAsset } from '@/lib/fetch-data'; import { PlusOutlined } from '@ant-design/icons'; import { Button, Form, App, Input, Modal, DatePicker } from 'antd'; import { FC, ReactNode, useEffect, useState } from 'react'; import dayjs from 'dayjs'; import germanLocale from 'antd/es/date-picker/locale/de_DE'; -import { useRouter } from 'next/navigation'; import { AuthCan } from '@/components/auth-can'; +import { addRole as serverAddRoles } from '@/lib/data/roles'; type PostRoleKeys = 'name' | 'description' | 'expiration'; @@ -16,35 +15,9 @@ const CreateRoleModal: FC<{ close: () => void; }> = ({ modalOpen, close }) => { const [form] = Form.useForm(); - const router = useRouter(); const { message: messageApi } = App.useApp(); type ErrorsObject = { [field in PostRoleKeys]?: ReactNode[] }; const [formatError, setFormatError] = useState({}); - const { mutateAsync: postRole, isLoading } = usePostAsset('/roles', { - onError(e) { - if (!(typeof e === 'object' && e !== null && 'errors' in e)) { - return; - } - - const errors: { [key in PostRoleKeys]?: ReactNode[] } = {}; - - function appendError(key: PostRoleKeys, error: string) { - if (key in errors) { - errors[key]!.push(

{error}

); - } else { - errors[key] = [

{error}

]; - } - } - - for (const error of e.errors as string[]) { - if (error.includes('name')) appendError('name', error); - else if (error.includes('description')) appendError('description', error); - else if (error.includes('expiration')) appendError('expiration', error); - } - - setFormatError(errors); - }, - }); const [submittable, setSubmittable] = useState(false); const values = Form.useWatch('name', form); @@ -71,15 +44,7 @@ const CreateRoleModal: FC<{ expiration = (values.expirationDayJs as dayjs.Dayjs).toISOString(); try { - const newRole = await postRole({ - body: { - name: values.name, - description: values.description, - expiration, - }, - }); - messageApi.success({ content: 'Role created' }); - router.push(`/iam/roles/${newRole.id}`); + await serverAddRoles(values); } catch (e) { messageApi.error({ content: 'Something went wrong' }); } @@ -122,7 +87,7 @@ const CreateRoleModal: FC<{ - diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx index ac3527173..d5ac947d8 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx @@ -1,11 +1,27 @@ -import Auth from '@/components/auth'; +import Auth, { getCurrentUser } from '@/components/auth'; +import Content from '@/components/content'; +import { getRoles } from '@/lib/data/legacy/iam/roles'; import RolesPage from './role-page'; +const Page = async () => { + const { ability } = await getCurrentUser(); + + const roles = getRoles(ability); + + return ( + + + + ); +}; + export default Auth( { action: 'manage', resource: 'Role', fallbackRedirect: '/', }, - RolesPage, + Page, ); + +export const dynamic = 'force-dynamic'; diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx index a1f11860d..172dea73e 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx @@ -1,11 +1,10 @@ 'use client'; -import { FC, useState } from 'react'; +import { useState } from 'react'; import { DeleteOutlined } from '@ant-design/icons'; -import { Space, Button, Result, Table, App } from 'antd'; -import { useGetAsset, useDeleteAsset, ApiData } from '@/lib/fetch-data'; +import { Space, Button, Table, App } from 'antd'; +import { ApiData } from '@/lib/fetch-data'; import { CloseOutlined } from '@ant-design/icons'; -import Content from '@/components/content'; import HeaderActions from './header-actions'; import useFuzySearch, { ReplaceKeysWithHighlighted } from '@/lib/useFuzySearch'; import Link from 'next/link'; @@ -15,18 +14,14 @@ import { useAbilityStore } from '@/lib/abilityStore'; import ConfirmationButton from '@/components/confirmation-button'; import { useRouter } from 'next/navigation'; import RoleSidePanel from './role-side-panel'; +import { deleteRoles as serverDeleteRoles } from '@/lib/data/roles'; type Role = ApiData<'/roles', 'get'>[number]; export type FilteredRole = ReplaceKeysWithHighlighted; -const RolesPage: FC = () => { +const RolesPage = ({ roles }: { roles: Role[] }) => { const { message: messageApi } = App.useApp(); const ability = useAbilityStore((store) => store.ability); - const { error, data: roles, isLoading, refetch: refetchRoles } = useGetAsset('/roles', {}); - const { mutateAsync: deleteRole, isLoading: deletingRole } = useDeleteAsset('/roles/{id}', { - onSuccess: () => refetchRoles(), - onError: () => messageApi.open({ type: 'error', content: 'Something went wrong' }), - }); const router = useRouter(); const { setSearchQuery, filteredData: filteredRoles } = useFuzySearch({ @@ -47,10 +42,16 @@ const RolesPage: FC = () => { (role) => !ability.can('delete', toCaslResource('Role', role)), ); - async function deleteRoles(userIds: string[]) { - setSelectedRowKeys([]); - setSelectedRows([]); - await Promise.allSettled(userIds.map((id) => deleteRole({ params: { path: { id } } }))); + async function deleteRoles(roleIds: string[]) { + try { + await serverDeleteRoles(roleIds); + + setSelectedRowKeys([]); + setSelectedRows([]); + router.refresh(); + } catch (e) { + messageApi.error((e as Error).message); + } } const columns = [ @@ -80,6 +81,7 @@ const RolesPage: FC = () => { title="Delete Role" description="Are you sure you want to delete this role?" onConfirm={() => deleteRoles([id])} + canCloseWhileLoading={true} buttonProps={{ disabled: !ability.can('delete', toCaslResource('Role', role)), style: { @@ -98,77 +100,61 @@ const RolesPage: FC = () => { }, ]; - if (error) - return ( - - ); - return ( - -
- } - leftNode={ - selectedRowKeys.length > 0 ? ( - -
); }; From 9a2dad58cc09154f9b87a8e40dccaa1f4b8ff470 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 13 Dec 2023 18:33:39 +0100 Subject: [PATCH 13/22] loading page and error hanlers --- .../app/(dashboard)/iam/error.tsx | 7 +++++++ .../app/(dashboard)/iam/loading.tsx | 19 +++++++++++++++++++ .../(dashboard)/iam/roles/[roleId]/page.tsx | 9 ++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/management-system-v2/app/(dashboard)/iam/error.tsx create mode 100644 src/management-system-v2/app/(dashboard)/iam/loading.tsx diff --git a/src/management-system-v2/app/(dashboard)/iam/error.tsx b/src/management-system-v2/app/(dashboard)/iam/error.tsx new file mode 100644 index 000000000..53aa6d349 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/iam/error.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { Result } from 'antd'; + +const Error = () => ; + +export default Error; diff --git a/src/management-system-v2/app/(dashboard)/iam/loading.tsx b/src/management-system-v2/app/(dashboard)/iam/loading.tsx new file mode 100644 index 000000000..e393bea0b --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/iam/loading.tsx @@ -0,0 +1,19 @@ +import Content from '@/components/content'; +import { Space, Spin } from 'antd'; +import { LoadingOutlined } from '@ant-design/icons'; + +const Loading = () => { + return ( + + + } /> + + + ); +}; + +export default Loading; diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx index c8265b20e..76cd40141 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/page.tsx @@ -1,7 +1,7 @@ import Auth, { getCurrentUser } from '@/components/auth'; import Content from '@/components/content'; import { getRoleById } from '@/lib/data/legacy/iam/roles'; -import { Button, Card, Space, Tabs } from 'antd'; +import { Button, Card, Result, Space, Tabs } from 'antd'; import { LeftOutlined } from '@ant-design/icons'; import Link from 'next/link'; import { ComponentProps } from 'react'; @@ -31,6 +31,13 @@ const Page = async ({ params: { roleId } }: { params: { roleId: string } }) => { ] : []; + if (!role) + return ( + + + + ); + return ( Date: Fri, 15 Dec 2023 16:02:52 +0100 Subject: [PATCH 14/22] Fixed permissions check --- src/management-system-v2/lib/data/legacy/iam/roles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/roles.js b/src/management-system-v2/lib/data/legacy/iam/roles.js index 450e9d68e..048bb2ceb 100644 --- a/src/management-system-v2/lib/data/legacy/iam/roles.js +++ b/src/management-system-v2/lib/data/legacy/iam/roles.js @@ -137,7 +137,7 @@ export function updateRole(roleId, roleRepresentation, ability) { // Casl isn't really built to check the value of input fields when updating, so we have to perform this two checks if ( !( - ability.checkInputFields(toCaslResource('Role', targetRole), 'update', targetRole) && + ability.checkInputFields(toCaslResource('Role', targetRole), 'update', roleRepresentation) && ability.can('create', toCaslResource('Role', roleRepresentation)) ) ) From 8bbfb1e8e55669b43c9c007ba2033f2373d3adc1 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Sun, 17 Dec 2023 16:19:54 +0100 Subject: [PATCH 15/22] Ms2: Role and Role-Mapping migrations --- .../api/auth/[...nextauth]/auth-options.ts | 21 +++++++++++++++- .../migrations/role-mappings-migrations.js | 12 +++++++++ .../legacy/iam/migrations/role-migrations.js | 5 ++++ .../lib/data/legacy/iam/role-mappings.js | 16 ++++++++---- .../lib/data/legacy/iam/roles.js | 25 ++++++++++++++++--- 5 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index d91703661..766c155f5 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -1,12 +1,31 @@ import { AuthOptions } from 'next-auth'; import { User } from '@/types/next-auth'; import { randomUUID } from 'crypto'; +import { getRoles } from '@/lib/data/legacy/iam/roles'; +import { addRoleMappings } from '@/lib/data/legacy/iam/role-mappings'; export const nextAuthOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, providers: [], callbacks: { - async jwt({ token, user, trigger }) { + async jwt({ token, user: _user, trigger, account }) { + const user = _user as User; + + if ( + process.env.NODE_ENV === 'development' && + account?.provider === 'auth0' && + user.username === 'admin' && + (trigger === 'signIn' || trigger === 'signUp') + ) { + const adminRole = getRoles().find((role) => role.name === '@admin'); + + if (!adminRole) throw new Error('Admin role not found'); + + if (!adminRole.members.find((member) => member.userId === user.id)) + // @ts-ignore + addRoleMappings([{ userId: user.id, roleId: adminRole.id }]); + } + if (trigger === 'signIn') token.csrfToken = randomUUID(); if (user) token.user = user as User; return token; diff --git a/src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js b/src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js new file mode 100644 index 000000000..1b122b2de --- /dev/null +++ b/src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js @@ -0,0 +1,12 @@ +// first seed of role-mappings db + +export const developmentRoleMappingsMigrations = [ + { + roleName: '@process_admin', + userId: 'development-id|johndoe', + }, + { + roleName: '@process_admin', + userId: 'development-id|admin', + }, +]; diff --git a/src/management-system-v2/lib/data/legacy/iam/migrations/role-migrations.js b/src/management-system-v2/lib/data/legacy/iam/migrations/role-migrations.js index 18d368ef2..91571d709 100644 --- a/src/management-system-v2/lib/data/legacy/iam/migrations/role-migrations.js +++ b/src/management-system-v2/lib/data/legacy/iam/migrations/role-migrations.js @@ -8,6 +8,7 @@ export const roleMigrations = [ description: 'Default role for all authenticated PROCEED users.', note: 'This role cannot be removed!', permissions: {}, + exipiration: null, }, { name: '@guest', @@ -15,6 +16,7 @@ export const roleMigrations = [ description: 'Default role for all unauthenticated PROCEED users.', note: 'This role cannot be removed!', permissions: {}, + exipiration: null, }, { name: '@admin', @@ -24,6 +26,7 @@ export const roleMigrations = [ permissions: { All: PERMISSION_ADMIN, }, + exipiration: null, }, { name: '@process_admin', @@ -35,6 +38,7 @@ export const roleMigrations = [ Project: PERMISSION_ADMIN, Template: PERMISSION_ADMIN, }, + exipiration: null, }, { name: '@environment_admin', @@ -45,5 +49,6 @@ export const roleMigrations = [ Setting: PERMISSION_ADMIN, EnvConfig: PERMISSION_ADMIN, }, + exipiration: null, }, ]; diff --git a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js index 0b2ba7795..b7510afc7 100644 --- a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js +++ b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js @@ -4,6 +4,8 @@ import { roleMetaObjects } from './roles.js'; import { ApiData, ApiRequestBody } from '@/lib/fetch-data'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { toCaslResource } from '@/lib/ability/caslAbility'; +import { developmentRoleMappingsMigrations } from './migrations/role-mappings-migrations.js'; +import { adminRules } from '@/lib/authorization/caslRules'; let firstInit = !global.roleMappingsMetaObjects; @@ -43,13 +45,15 @@ export function getRoleMappings(ability) { * Returns a role mapping by user id * * @param {String} userId - the id of a user - * @param {Ability} ability + * @param {Ability} [ability] * * @returns {ApiData<'/role-mappings/users/{userId}','get'>} - role mappings of a user */ export function getRoleMappingByUserId(userId, ability) { - const roleMappings = roleMappingsMetaObjects.users[userId]; - return ability.filter('view', 'RoleMapping', roleMappings); + const userRoleMappings = roleMappingsMetaObjects.users[userId]; + + if (!ability) return userRoleMappings; + return ability.filter('view', 'RoleMapping', userRoleMappings); } // TODO: also check if user exists? @@ -57,10 +61,12 @@ export function getRoleMappingByUserId(userId, ability) { * Adds a user role mapping * * @param {ApiRequestBody<'/role-mappings','post'>} roleMappings - role mapping object containing userId & roleId - * @param {Ability} ability + * @param {Ability} [ability] */ export async function addRoleMappings(roleMappings, ability) { - const allowedRoleMappings = ability.filter('create', 'RoleMapping', roleMappings); + const allowedRoleMappings = ability + ? ability.filter('create', 'RoleMapping', roleMappings) + : roleMappings; allowedRoleMappings.forEach((roleMapping) => { const { roleId, userId } = roleMapping; diff --git a/src/management-system-v2/lib/data/legacy/iam/roles.js b/src/management-system-v2/lib/data/legacy/iam/roles.js index 048bb2ceb..9fa58af16 100644 --- a/src/management-system-v2/lib/data/legacy/iam/roles.js +++ b/src/management-system-v2/lib/data/legacy/iam/roles.js @@ -2,10 +2,12 @@ import { v4 } from 'uuid'; import store from '../store.js'; import { roleMigrations } from './migrations/role-migrations.js'; import { mergeIntoObject } from '../../../helpers/javascriptHelpers'; -import { roleMappingsMetaObjects } from './role-mappings.js'; +import { addRoleMappings, roleMappingsMetaObjects } from './role-mappings.js'; import { ApiData, ApiRequestBody } from '@/lib/fetch-data'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { toCaslResource } from '@/lib/ability/caslAbility'; +import { adminRules } from '@/lib/authorization/caslRules.ts'; +import { developmentRoleMappingsMigrations } from './migrations/role-mappings-migrations.js'; let firstInit = !global.roleMetaObjects; @@ -26,8 +28,22 @@ export function init() { // migrate roles roleMigrations.forEach((role) => { + debugger; const index = storedRoles.findIndex((storedRole) => storedRole.name === role.name); - if (index < 0) addRole(role); + if (index >= 0) return; + + const { id: roleId, name } = addRole(role, new Ability(adminRules())); + + if (process.env.NODE_ENV === 'development') { + const roleMappings = developmentRoleMappingsMigrations + .filter((mapping) => mapping.roleName === name) + .map((mapping) => ({ + roleId, + userId: mapping.userId, + })); + + addRoleMappings(roleMappings, new Ability(adminRules())); + } }); // set roles store cache for quick access @@ -38,14 +54,14 @@ init(); /** * Returns all roles in form of an array * - * @param {Ability} ability + * @param {Ability} [ability] * * @returns {ApiData<'/roles','get'>} - array containing all roles */ export function getRoles(ability) { const roles = Object.values(roleMetaObjects); - return ability.filter('view', 'Process', roles); + return ability ? ability.filter('view', 'Process', roles) : roles; } /** @@ -78,6 +94,7 @@ export function getRoleById(roleId, ability) { * @returns {ApiData<'/roles/{id}','get'> } - role object */ export function addRole(roleRepresentation, ability) { + debugger; if (!ability.can('create', toCaslResource('Role', roleRepresentation))) throw new UnauthorizedError(); From 8617c0f611dd4f027b2febb78d61a4187712e2bd Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 18 Dec 2023 09:46:11 +0100 Subject: [PATCH 16/22] Fix: Dev admin should have admin role --- .../lib/data/legacy/iam/migrations/role-mappings-migrations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js b/src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js index 1b122b2de..0a365f989 100644 --- a/src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js +++ b/src/management-system-v2/lib/data/legacy/iam/migrations/role-mappings-migrations.js @@ -6,7 +6,7 @@ export const developmentRoleMappingsMigrations = [ userId: 'development-id|johndoe', }, { - roleName: '@process_admin', + roleName: '@admin', userId: 'development-id|admin', }, ]; From cd2b4bb49bcc97c30213720c59f799e03e44ac46 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 18 Dec 2023 09:47:19 +0100 Subject: [PATCH 17/22] Remove debugger calls --- src/management-system-v2/lib/data/legacy/iam/roles.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/roles.js b/src/management-system-v2/lib/data/legacy/iam/roles.js index 9fa58af16..4a22a05d2 100644 --- a/src/management-system-v2/lib/data/legacy/iam/roles.js +++ b/src/management-system-v2/lib/data/legacy/iam/roles.js @@ -28,7 +28,6 @@ export function init() { // migrate roles roleMigrations.forEach((role) => { - debugger; const index = storedRoles.findIndex((storedRole) => storedRole.name === role.name); if (index >= 0) return; @@ -94,7 +93,6 @@ export function getRoleById(roleId, ability) { * @returns {ApiData<'/roles/{id}','get'> } - role object */ export function addRole(roleRepresentation, ability) { - debugger; if (!ability.can('create', toCaslResource('Role', roleRepresentation))) throw new UnauthorizedError(); From 7dc213f8e9015bc7d6fc16c87ef0e24dc5d310ae Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 18 Dec 2023 09:48:05 +0100 Subject: [PATCH 18/22] Update page when role changes --- .../app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx index ac0ba6dc4..d3115574f 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx @@ -8,6 +8,7 @@ import dayjs from 'dayjs'; import germanLocale from 'antd/es/date-picker/locale/de_DE'; import { useAbilityStore } from '@/lib/abilityStore'; import { updateRole } from '@/lib/data/roles'; +import { useRouter } from 'next/navigation'; type Role = ApiData<'/roles/{id}', 'get'>; @@ -15,6 +16,7 @@ const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => { const { message } = App.useApp(); const ability = useAbilityStore((store) => store.ability); const [form] = Form.useForm(); + const router = useRouter(); const [submittable, setSubmittable] = useState(false); const values = Form.useWatch('name', form); @@ -40,6 +42,7 @@ const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => { try { await updateRole(role.id, values); + router.refresh(); message.open({ type: 'success', content: 'Role updated' }); } catch (e) { let msg = 'Something went wrong'; From f369c37c83a51943e68f1063bb61b217e8028821 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 20 Dec 2023 11:47:58 +0100 Subject: [PATCH 19/22] Removed console.log --- src/management-system-v2/lib/data/legacy/iam/roles.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/roles.js b/src/management-system-v2/lib/data/legacy/iam/roles.js index 4a22a05d2..337d5909b 100644 --- a/src/management-system-v2/lib/data/legacy/iam/roles.js +++ b/src/management-system-v2/lib/data/legacy/iam/roles.js @@ -212,9 +212,4 @@ export async function deleteRole(roleId, ability) { users: { ...roleMappingsMetaObjects.users }, }, }); - - console.log( - 'leftkeys', - Object.keys(roleMetaObjects).map((r) => roleMetaObjects[r].name), - ); } From b551b074e4feb9924f745b10afb5c0d74fd82ecc Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 20 Dec 2023 11:58:33 +0100 Subject: [PATCH 20/22] Removed unneeded asyncs and cleaned imports --- .../lib/data/legacy/iam/role-mappings.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js index b7510afc7..ad052c3df 100644 --- a/src/management-system-v2/lib/data/legacy/iam/role-mappings.js +++ b/src/management-system-v2/lib/data/legacy/iam/role-mappings.js @@ -4,8 +4,6 @@ import { roleMetaObjects } from './roles.js'; import { ApiData, ApiRequestBody } from '@/lib/fetch-data'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { toCaslResource } from '@/lib/ability/caslAbility'; -import { developmentRoleMappingsMigrations } from './migrations/role-mappings-migrations.js'; -import { adminRules } from '@/lib/authorization/caslRules'; let firstInit = !global.roleMappingsMetaObjects; @@ -63,7 +61,7 @@ export function getRoleMappingByUserId(userId, ability) { * @param {ApiRequestBody<'/role-mappings','post'>} roleMappings - role mapping object containing userId & roleId * @param {Ability} [ability] */ -export async function addRoleMappings(roleMappings, ability) { +export function addRoleMappings(roleMappings, ability) { const allowedRoleMappings = ability ? ability.filter('create', 'RoleMapping', roleMappings) : roleMappings; @@ -134,7 +132,7 @@ export async function addRoleMappings(roleMappings, ability) { * * @returns {Object} - new mapping object without removed element */ -export async function deleteRoleMapping(userId, roleId, ability) { +export function deleteRoleMapping(userId, roleId, ability) { const roleMapping = roleMappingsMetaObjects.users[userId].find( (roleMapping) => roleMapping.roleId === roleId, ); From e83d172f8f7ee3fd7ce84fa7156adc305d6e7f18 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 20 Dec 2023 12:30:30 +0100 Subject: [PATCH 21/22] Implemented userError --- .../iam/roles/[roleId]/roleGeneralData.tsx | 10 ++--- .../iam/roles/[roleId]/rolePermissions.tsx | 3 +- .../(dashboard)/iam/roles/header-actions.tsx | 3 +- .../app/(dashboard)/iam/roles/role-page.tsx | 5 ++- src/management-system-v2/lib/data/roles.ts | 40 +++++++++++-------- 5 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx index d3115574f..f5d05fd3c 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx @@ -41,14 +41,12 @@ const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => { } try { - await updateRole(role.id, values); + const result = await updateRole(role.id, values); + if (result && 'error' in result) throw new Error(); router.refresh(); message.open({ type: 'success', content: 'Role updated' }); - } catch (e) { - let msg = 'Something went wrong'; - if (e instanceof Error && e.message) msg += `: ${e.message}`; - - message.open({ type: 'error', content: msg }); + } catch (_) { + message.open({ type: 'error', content: 'Something went wrong' }); } } diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx index e1bd35eff..4cae35a08 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/rolePermissions.tsx @@ -269,9 +269,10 @@ const RolePermissions: FC<{ role: Role }> = ({ role }) => { async function updateRole() { setLoading(true); try { - await serverUpdateRole(role.id, { + const result = await serverUpdateRole(role.id, { permissions, }); + if (result && 'error' in result) throw new Error(); message.open({ content: 'Role updated', type: 'success' }); } catch (e) { diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx index 2d6556232..3192b5610 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx @@ -44,7 +44,8 @@ const CreateRoleModal: FC<{ expiration = (values.expirationDayJs as dayjs.Dayjs).toISOString(); try { - await serverAddRoles(values); + const result = await serverAddRoles(values); + if (result && 'error' in result) throw new Error(); } catch (e) { messageApi.error({ content: 'Something went wrong' }); } diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx index 172dea73e..84c71eb63 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx @@ -44,13 +44,14 @@ const RolesPage = ({ roles }: { roles: Role[] }) => { async function deleteRoles(roleIds: string[]) { try { - await serverDeleteRoles(roleIds); + const result = await serverDeleteRoles(roleIds); + if (result && 'error' in result) throw new Error(); setSelectedRowKeys([]); setSelectedRows([]); router.refresh(); } catch (e) { - messageApi.error((e as Error).message); + messageApi.error({ content: 'Something went wrong' }); } } diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts index 6b37b1d28..a5fc24335 100644 --- a/src/management-system-v2/lib/data/roles.ts +++ b/src/management-system-v2/lib/data/roles.ts @@ -2,33 +2,41 @@ import { getCurrentUser } from '@/components/auth'; import { deleteRole, addRole as _addRole, updateRole as _updateRole } from './legacy/iam/roles.js'; -import { redirect } from 'next/navigation.js'; +import { redirect } from 'next/navigation'; +import { userError } from '../user-error'; + +import { RedirectType } from 'next/dist/client/components/redirect'; export async function deleteRoles(roleIds: string[]) { const { ability } = await getCurrentUser(); - const errors: { roleId: string; error: Error }[] = []; - - for (const roleId of roleIds) { - try { + try { + for (const roleId of roleIds) { deleteRole(roleId, ability); - } catch (error) { - errors.push({ roleId, error: error as Error }); } + } catch (_) { + return userError('Error deleting roles'); } - - return errors; } export async function addRole(role: Parameters[0]) { - const { ability } = await getCurrentUser(); - - const newRole = _addRole(role, ability); - redirect(`/iam/roles/${newRole.id}`); + let newRoleId; + try { + const { ability } = await getCurrentUser(); + + const newRole = _addRole(role, ability); + newRoleId = newRole.id; + } catch (e) { + return userError('Error adding role'); + } + redirect(`/iam/roles/${newRoleId}`, RedirectType.push); } export async function updateRole(roleId: string, updatedRole: Parameters[1]) { - const { ability } = await getCurrentUser(); - - _updateRole(roleId, updatedRole, ability); + try { + const { ability } = await getCurrentUser(); + _updateRole(roleId, updatedRole, ability); + } catch (e) { + return userError('Error updating role'); + } } From 31c53014d94194aaba13d0983c01e09085b7acc7 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 20 Dec 2023 12:31:52 +0100 Subject: [PATCH 22/22] Remove unnecessary dynamic --- src/management-system-v2/app/(dashboard)/iam/roles/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx index d5ac947d8..eeebe7489 100644 --- a/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx +++ b/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx @@ -23,5 +23,3 @@ export default Auth( }, Page, ); - -export const dynamic = 'force-dynamic';