chore: Move frontend authorization to permission based system (#9709)

We previously relied on user roles to determine whether to render
specific routes in our frontend components. A permissions-based model is replacing this approach.


Follow up: #9695

Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose
2024-07-03 15:13:16 -07:00
committed by GitHub
co-authored by Pranav
parent 5520bf68f3
commit cc4851b19d
37 changed files with 582 additions and 229 deletions
@@ -0,0 +1,34 @@
export const hasPermissions = (
requiredPermissions = [],
availablePermissions = []
) => {
return requiredPermissions.some(permission =>
availablePermissions.includes(permission)
);
};
const isPermissionsPresentInRoute = route =>
route.meta && route.meta.permissions;
export const buildPermissionsFromRouter = (routes = []) =>
routes.reduce((acc, route) => {
if (route.name) {
if (!isPermissionsPresentInRoute(route)) {
// eslint-disable-next-line
console.error(route);
throw new Error(
"The route doesn't have the required permissions defined"
);
}
acc[route.name] = route.meta.permissions;
}
if (route.children) {
acc = {
...acc,
...buildPermissionsFromRouter(route.children),
};
}
return acc;
}, {});
@@ -1,19 +1,16 @@
import { hasPermissions } from './permissionsHelper';
// eslint-disable-next-line default-param-last
export const getCurrentAccount = ({ accounts } = {}, accountId) => {
return accounts.find(account => account.id === accountId);
};
// eslint-disable-next-line default-param-last
export const getUserRole = ({ accounts } = {}, accountId) => {
const currentAccount = getCurrentAccount({ accounts }, accountId) || {};
return currentAccount.role || null;
export const routeIsAccessibleFor = (route, userPermissions = []) => {
const { meta: { permissions: routePermissions = [] } = {} } = route;
return hasPermissions(routePermissions, userPermissions);
};
export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => {
return roleWiseRoutes[role].includes(route);
};
const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
const validateActiveAccountRoutes = (to, user) => {
// If the current account is active, then check for the route permissions
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
@@ -22,15 +19,13 @@ const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
return accountDashboardURL;
}
const userRole = getUserRole(user, Number(to.params.accountId));
const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes);
const isAccessible = routeIsAccessibleFor(to, user.permissions);
// If the route is not accessible for the user, return to dashboard screen
return isAccessible ? null : accountDashboardURL;
};
export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
export const validateLoggedInRoutes = (to, user) => {
const currentAccount = getCurrentAccount(user, Number(to.params.accountId));
// If current account is missing, either user does not have
// access to the account or the account is deleted, return to login screen
if (!currentAccount) {
@@ -40,7 +35,7 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
const isCurrentAccountActive = currentAccount.status === 'active';
if (isCurrentAccountActive) {
return validateActiveAccountRoutes(to, user, roleWiseRoutes);
return validateActiveAccountRoutes(to, user);
}
// If the current account is not active, then redirect the user to the suspended screen
@@ -0,0 +1,84 @@
import {
buildPermissionsFromRouter,
hasPermissions,
} from '../permissionsHelper';
describe('hasPermissions', () => {
it('returns true if permission is present', () => {
expect(
hasPermissions(['contact_manage'], ['team_manage', 'contact_manage'])
).toBe(true);
});
it('returns true if permission is not present', () => {
expect(
hasPermissions(['contact_manage'], ['team_manage', 'user_manage'])
).toBe(false);
expect(hasPermissions()).toBe(false);
expect(hasPermissions([])).toBe(false);
});
});
describe('buildPermissionsFromRouter', () => {
it('returns a valid object when routes have permissions defined', () => {
expect(
buildPermissionsFromRouter([
{
path: 'agent',
name: 'agent_list',
meta: { permissions: ['agent_admin'] },
},
{
path: 'inbox',
children: [
{
path: '',
name: 'inbox_list',
meta: { permissions: ['inbox_admin'] },
},
],
},
{
path: 'conversations',
children: [
{
path: '',
children: [
{
path: 'attachments',
name: 'attachments_list',
meta: { permissions: ['conversation_admin'] },
},
],
},
],
},
])
).toEqual({
agent_list: ['agent_admin'],
inbox_list: ['inbox_admin'],
attachments_list: ['conversation_admin'],
});
});
it('throws an error if a named routed does not have permissions defined', () => {
expect(() => {
buildPermissionsFromRouter([
{
path: 'agent',
name: 'agent_list',
},
]);
}).toThrow("The route doesn't have the required permissions defined");
expect(() => {
buildPermissionsFromRouter([
{
path: 'agent',
name: 'agent_list',
meta: {},
},
]);
}).toThrow("The route doesn't have the required permissions defined");
});
});
@@ -1,7 +1,6 @@
import {
getConversationDashboardRoute,
getCurrentAccount,
getUserRole,
isAConversationRoute,
routeIsAccessibleFor,
validateLoggedInRoutes,
@@ -15,24 +14,11 @@ describe('#getCurrentAccount', () => {
});
});
describe('#getUserRole', () => {
it('should return the current role', () => {
expect(
getUserRole({ accounts: [{ id: 1, role: 'administrator' }] }, 1)
).toEqual('administrator');
expect(getUserRole({ accounts: [] }, 1)).toEqual(null);
});
});
describe('#routeIsAccessibleFor', () => {
it('should return the correct access', () => {
const roleWiseRoutes = { agent: ['conversations'], admin: ['billing'] };
expect(routeIsAccessibleFor('billing', 'agent', roleWiseRoutes)).toEqual(
false
);
expect(routeIsAccessibleFor('billing', 'admin', roleWiseRoutes)).toEqual(
true
);
let route = { meta: { permissions: ['administrator'] } };
expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false);
expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true);
});
});
@@ -40,11 +26,7 @@ describe('#validateLoggedInRoutes', () => {
describe('when account access is missing', () => {
it('should return the login route', () => {
expect(
validateLoggedInRoutes(
{ params: { accountId: 1 } },
{ accounts: [] },
{}
)
validateLoggedInRoutes({ params: { accountId: 1 } }, { accounts: [] })
).toEqual(`app/login`);
});
});
@@ -53,9 +35,12 @@ describe('#validateLoggedInRoutes', () => {
it('return suspended route', () => {
expect(
validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] },
{ agent: ['conversations'] }
{
name: 'conversations',
params: { accountId: 1 },
meta: { permissions: ['agent'] },
},
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }
)
).toEqual(`accounts/1/suspended`);
});
@@ -65,9 +50,22 @@ describe('#validateLoggedInRoutes', () => {
it('returns null (no action required)', () => {
expect(
validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
{ agent: ['conversations'] }
{
name: 'conversations',
params: { accountId: 1 },
meta: { permissions: ['agent'] },
},
{
permissions: ['agent'],
accounts: [
{
id: 1,
role: 'agent',
permissions: ['agent'],
status: 'active',
},
],
}
)
).toEqual(null);
});
@@ -76,9 +74,12 @@ describe('#validateLoggedInRoutes', () => {
it('returns dashboard url', () => {
expect(
validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
{ admin: ['conversations'], agent: [] }
{
name: 'billing',
params: { accountId: 1 },
meta: { permissions: ['administrator'] },
},
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
)
).toEqual(`accounts/1/dashboard`);
});
@@ -88,8 +89,7 @@ describe('#validateLoggedInRoutes', () => {
expect(
validateLoggedInRoutes(
{ name: 'account_suspended', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
{ agent: ['account_suspended'] }
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
)
).toEqual(`accounts/1/dashboard`);
});