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:
@@ -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`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user