Skip to content

Commit

Permalink
Add User and team RBAC tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
lgalis committed Aug 26, 2024
1 parent 6304d7d commit ad35330
Show file tree
Hide file tree
Showing 11 changed files with 570 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EdaCredentialType } from '../../../interfaces/EdaCredentialType';
import { useEdaMultiSelectListView } from '../../../common/useEdaMultiSelectListView';
import { edaAPI } from '../../../common/eda-utils';
import styled from 'styled-components';
import { EdaEventStream } from '../../../interfaces/EdaEventStream';

export type EdaResourceType =
| EdaActivationInstance
Expand All @@ -24,7 +25,8 @@ export type EdaResourceType =
| EdaRulebookActivation
| EdaRuleAudit
| EdaProject
| EdaCredentialType;
| EdaCredentialType
| EdaEventStream;

const resourceToEndpointMapping: { [key: string]: string } = {
'eda.edacredential': edaAPI`/eda-credentials/`,
Expand All @@ -35,6 +37,7 @@ const resourceToEndpointMapping: { [key: string]: string } = {
'eda.credentialtype': edaAPI`/credential-types/`,
'eda.decisionenvironment': edaAPI`/decision-environments/`,
'eda.auditrule': edaAPI`/audit-rules/`,
'eda.eventstream': edaAPI`/event-streams/`,
};

const StyledTitle = styled(Title)`
Expand All @@ -57,6 +60,7 @@ export function EdaSelectResourcesStep() {
'eda.credentialtype': t('Select credential types'),
'eda.decisionenvironment': t('Select decision environments'),
'eda.auditrule': t('Select audit rules'),
'eda.eventstream': t('Select event stream'),
};
}, [t]);
const tableColumns = useMemo<ITableColumn<EdaResourceType>[]>(
Expand Down
4 changes: 4 additions & 0 deletions frontend/eda/access/roles/hooks/useEdaRoleMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export function useEdaRoleMetadata(): EdaRoleMetadata {
'shared.change_team': t('Change team'),
'shared.delete_team': t('Delete team'),
'shared.view_team': t('View team'),
'eda.add_eventstream': t('Add event stream'),
'eda.change_eventstream': t('Change event stream'),
'eda.delete_eventstream': t('Delete event stream'),
'eda.view_eventstream': t('View event stream'),
},
},
'eda.project': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export function EventStreamPage() {
tabs={[
{ label: t('Details'), page: EdaRoute.EventStreamDetails },
{ label: t('Activations'), page: EdaRoute.EventStreamActivations },
{ label: t('Team Access'), page: EdaRoute.EventStreamTeamAccess },
{ label: t('User Access'), page: EdaRoute.EventStreamUserAccess },
]}
params={{ id: eventStream?.id }}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useParams } from 'react-router-dom';
import { EdaRoute } from '../../main/EdaRoutes';
import { TeamAccess } from '../../../common/access/components/TeamAccess';

export function EventStreamTeamAccess() {
const params = useParams<{ id: string }>();
return (
<TeamAccess
service="eda"
id={params.id || ''}
type={'eventstream'}
addRolesRoute={EdaRoute.EventStreamAddTeams}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useParams } from 'react-router-dom';
import { EdaRoute } from '../../main/EdaRoutes';
import { UserAccess } from '../../../common/access/components/UserAccess';

export function EventStreamUserAccess() {
const params = useParams<{ id: string }>();
return (
<UserAccess
service="eda"
id={params.id || ''}
type={'project'}
addRolesRoute={EdaRoute.EventStreamAddUsers}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { edaAPI } from '../../common/eda-utils';
import { EdaEventStreamAddTeams } from './EdaEventStreamAddTeams';

describe('EdaEventStreamAddTeams', () => {
const component = <EdaEventStreamAddTeams />;
const path = '/event-streams/:id/team-access/add-teams';
const initialEntries = [`/event-streams/1/team-access/add-teams`];
const params = {
path,
initialEntries,
};

beforeEach(() => {
cy.intercept('GET', edaAPI`/event-streams/*`, { fixture: 'edaEventStream.json' });
cy.intercept('GET', edaAPI`/teams/?order_by=name*`, { fixture: 'edaTeams.json' });
cy.intercept('GET', edaAPI`/role_definitions/?content_type__model=event-stream*`, {
fixture: 'edaEventStreamRoles.json',
});
cy.mount(component, params);
});
it('should render with correct steps', () => {
cy.get('[data-cy="wizard-nav"] li').eq(0).should('contain.text', 'Select team(s)');
cy.get('[data-cy="wizard-nav"] li').eq(1).should('contain.text', 'Select roles to apply');
cy.get('[data-cy="wizard-nav"] li').eq(2).should('contain.text', 'Review');
cy.get('[data-cy="wizard-nav-item-teams"] button').should('have.class', 'pf-m-current');
cy.get('table tbody').find('tr').should('have.length', 4);
});
it('can filter teams by name', () => {
cy.intercept(edaAPI`/teams/?name=Gal*`, { fixtures: 'edaTeams.json' }).as('nameFilterRequest');
cy.filterTableByText('Gal');
cy.wait('@nameFilterRequest');
cy.clearAllFilters();
});
it('should validate that at least one team is selected for moving to next step', () => {
cy.get('table tbody').find('tr').should('have.length', 4);
cy.clickButton(/^Next$/);
cy.get('.pf-v5-c-alert__title').should('contain.text', 'Select at least one team.');
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-teams"] button').should('not.have.class', 'pf-m-current');
cy.get('[data-cy="wizard-nav-item-roles"] button').should('have.class', 'pf-m-current');
});
it('should validate that at least one role is selected for moving to Review step', () => {
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-roles"] button').should('have.class', 'pf-m-current');
cy.clickButton(/^Next$/);
cy.get('.pf-v5-c-alert__title').should('contain.text', 'Select at least one role.');
cy.selectTableRowByCheckbox('name', 'EventStream Admin', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-roles"] button').should('not.have.class', 'pf-m-current');
cy.get('[data-cy="wizard-nav-item-review"] button').should('have.class', 'pf-m-current');
});
it('should display selected team and role in the Review step', () => {
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.selectTableRowByCheckbox('name', 'EventStream Admin', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.get('[data-cy="wizard-nav-item-review"] button').should('have.class', 'pf-m-current');
cy.get('[data-cy="expandable-section-teams"]').should('contain.text', 'Teams');
cy.get('[data-cy="expandable-section-teams"]').should('contain.text', '1');
cy.get('[data-cy="expandable-section-teams"]').should('contain.text', 'Demo');
cy.get('[data-cy="expandable-section-edaRoles"]').should('contain.text', 'Roles');
cy.get('[data-cy="expandable-section-edaRoles"]').should('contain.text', '1');
cy.get('[data-cy="expandable-section-edaRoles"]').should('contain.text', 'EventStream Admin');
cy.get('[data-cy="expandable-section-edaRoles"]').should(
'contain.text',
'Has all permissions to a single event-stream and its child resources - rulebook'
);
});
it('should trigger bulk action dialog on submit', () => {
cy.intercept('POST', edaAPI`/role_team_assignments/`, {
statusCode: 201,
body: { team: 3, role_definition: 14, content_type: 'eda.event-stream', object_id: 1 },
}).as('createRoleAssignment');
cy.selectTableRowByCheckbox('name', 'Demo', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.selectTableRowByCheckbox('name', 'EventStream Admin', { disableFilter: true });
cy.clickButton(/^Next$/);
cy.clickButton(/^Finish$/);
cy.wait('@createRoleAssignment');
// Bulk action modal is displayed with success
cy.get('.pf-v5-c-modal-box').within(() => {
cy.get('table tbody').find('tr').should('have.length', 1);
cy.get('table tbody').should('contain.text', 'Demo');
cy.get('table tbody').should('contain.text', 'EventStream Admin');
cy.get('div.pf-v5-c-progress__description').should('contain.text', 'Success');
cy.get('div.pf-v5-c-progress__status').should('contain.text', '100%');
});
});
});
158 changes: 158 additions & 0 deletions frontend/eda/event-streams/components/EdaEventStreamAddTeams.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import {
LoadingPage,
PageHeader,
PageLayout,
PageWizard,
PageWizardStep,
useGetPageUrl,
usePageNavigate,
} from '../../../../framework';
import { RoleAssignmentsReviewStep } from '../../../common/access/RolesWizard/steps/RoleAssignmentsReviewStep';
import { postRequest } from '../../../common/crud/Data';
import { useGet } from '../../../common/crud/useGet';
import { EdaSelectRolesStep } from '../../access/common/EdaRolesWizardSteps/EdaSelectRolesStep';
import { EdaSelectTeamsStep } from '../../access/common/EdaRolesWizardSteps/EdaSelectTeamsStep';
import { edaAPI } from '../../common/eda-utils';
import { edaErrorAdapter } from '../../common/edaErrorAdapter';
import { useEdaBulkActionDialog } from '../../common/useEdaBulkActionDialog';
import { EdaEventStream } from '../../interfaces/EdaEventStream';
import { EdaRbacRole } from '../../interfaces/EdaRbacRole';
import { EdaTeam } from '../../interfaces/EdaTeam';
import { EdaRoute } from '../../main/EdaRoutes';

interface WizardFormValues {
teams: EdaTeam[];
edaRoles: EdaRbacRole[];
}

interface TeamRolePair {
team: EdaTeam;
role: EdaRbacRole;
}

export function EdaEventStreamAddTeams() {
const { t } = useTranslation();
const getPageUrl = useGetPageUrl();
const params = useParams<{ id: string }>();
const pageNavigate = usePageNavigate();
const { data: eventstream, isLoading } = useGet<EdaEventStream>(
edaAPI`/event-streams/${params.id ?? ''}/`
);
const teamRoleProgressDialog = useEdaBulkActionDialog<TeamRolePair>();

if (isLoading || !eventstream) return <LoadingPage />;

const steps: PageWizardStep[] = [
{
id: 'teams',
label: t('Select team(s)'),
inputs: (
<EdaSelectTeamsStep
descriptionForTeamsSelection={t(
'Select the team(s) that you want to give access to {{eventstreamName}}.',
{
eventstreamName: eventstream?.name,
}
)}
/>
),
validate: (formData, _) => {
const { teams } = formData as { teams: EdaTeam[] };
if (!teams?.length) {
throw new Error(t('Select at least one team.'));
}
},
},
{
id: 'roles',
label: t('Select roles to apply'),
inputs: (
<EdaSelectRolesStep
contentType="eventstream"
fieldNameForPreviousStep="teams"
descriptionForRoleSelection={t('Choose roles to apply to {{eventstreamName}}.', {
eventstreamName: eventstream?.name,
})}
/>
),
validate: (formData, _) => {
const { edaRoles } = formData as { edaRoles: EdaRbacRole[] };
if (!edaRoles?.length) {
throw new Error(t('Select at least one role.'));
}
},
},
{
id: 'review',
label: t('Review'),
inputs: <RoleAssignmentsReviewStep />,
},
];

const onSubmit = async (data: WizardFormValues) => {
const { teams, edaRoles } = data;
const items: TeamRolePair[] = [];
for (const team of teams) {
for (const role of edaRoles) {
items.push({ team, role });
}
}
return new Promise<void>((resolve) => {
teamRoleProgressDialog({
title: t('Add roles'),
keyFn: ({ team, role }) => `${team.id}_${role.id}`,
items,
actionColumns: [
{ header: t('Team'), cell: ({ team }) => team.name },
{ header: t('Role'), cell: ({ role }) => role.name },
],
actionFn: ({ team, role }) =>
postRequest(edaAPI`/role_team_assignments/`, {
team: team.id,
role_definition: role.id,
content_type: 'eda.eventstream',
object_id: eventstream.id,
}),
onComplete: () => {
resolve();
},
onClose: () => {
pageNavigate(EdaRoute.EventStreamTeamAccess, {
params: { id: eventstream.id.toString() },
});
},
});
});
};

return (
<PageLayout>
<PageHeader
title={t('Add roles')}
breadcrumbs={[
{ label: t('EventStreams'), to: getPageUrl(EdaRoute.EventStreams) },
{
label: eventstream?.name,
to: getPageUrl(EdaRoute.EventStreamDetails, { params: { id: eventstream?.id } }),
},
{
label: t('Team Access'),
to: getPageUrl(EdaRoute.EventStreamTeamAccess, { params: { id: eventstream?.id } }),
},
{ label: t('Add roles') },
]}
/>
<PageWizard<WizardFormValues>
errorAdapter={edaErrorAdapter}
steps={steps}
onSubmit={onSubmit}
disableGrid
onCancel={() => {
pageNavigate(EdaRoute.EventStreamTeamAccess, { params: { id: eventstream?.id } });
}}
/>
</PageLayout>
);
}
Loading

0 comments on commit ad35330

Please sign in to comment.