Skip to content

Commit

Permalink
Add application access section to the role editor
Browse files Browse the repository at this point in the history
  • Loading branch information
bl-nero committed Oct 22, 2024
1 parent eabbf32 commit 64a67e3
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 21 deletions.
69 changes: 55 additions & 14 deletions web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ import { createTeleportContext } from 'teleport/mocks/contexts';

import {
AccessSpec,
AppAccessSpec,
KubernetesAccessSpec,
newAccessSpec,
newRole,
roleToRoleEditorModel,
ServerAccessSpec,
StandardEditorModel,
} from './standardmodel';
import {
AppAccessSpecSection,
KubernetesAccessSpecSection,
SectionProps,
ServerAccessSpecSection,
Expand Down Expand Up @@ -69,15 +72,19 @@ test('adding and removing sections', async () => {
await user.click(
screen.getByRole('button', { name: 'Add New Specifications' })
);
expect(getAllMenuItemNames()).toEqual(['Kubernetes', 'Servers']);
expect(getAllMenuItemNames()).toEqual([
'Kubernetes',
'Servers',
'Applications',
]);

await user.click(screen.getByRole('menuitem', { name: 'Servers' }));
expect(getAllSectionNames()).toEqual(['Role Metadata', 'Servers']);

await user.click(
screen.getByRole('button', { name: 'Add New Specifications' })
);
expect(getAllMenuItemNames()).toEqual(['Kubernetes']);
expect(getAllMenuItemNames()).toEqual(['Kubernetes', 'Applications']);

await user.click(screen.getByRole('menuitem', { name: 'Kubernetes' }));
expect(getAllSectionNames()).toEqual([
Expand Down Expand Up @@ -154,17 +161,13 @@ const StatefulSection = <S extends AccessSpec>({
);
};

test('editing server access specs', async () => {
test('ServerAccessSpecSection', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<StatefulSection<ServerAccessSpec>
component={ServerAccessSpecSection}
defaultValue={{
kind: 'node',
labels: [],
logins: [],
}}
defaultValue={newAccessSpec('node')}
onChange={onChange}
/>
);
Expand Down Expand Up @@ -194,12 +197,7 @@ describe('KubernetesAccessSpecSection', () => {
render(
<StatefulSection<KubernetesAccessSpec>
component={KubernetesAccessSpecSection}
defaultValue={{
kind: 'kube_cluster',
groups: [],
labels: [],
resources: [],
}}
defaultValue={newAccessSpec('kube_cluster')}
onChange={onChange}
/>
);
Expand Down Expand Up @@ -311,6 +309,49 @@ describe('KubernetesAccessSpecSection', () => {
});
});

test('AppAccessSpecSection', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<StatefulSection<AppAccessSpec>
component={AppAccessSpecSection}
defaultValue={newAccessSpec('app')}
onChange={onChange}
/>
);

await user.click(screen.getByRole('button', { name: 'Add a Label' }));
await user.type(screen.getByPlaceholderText('label key'), 'env');
await user.type(screen.getByPlaceholderText('label value'), 'prod');
await user.type(
within(screen.getByRole('group', { name: 'AWS Role ARNs' })).getByRole(
'textbox'
),
'arn:aws:iam::123456789012:role/admin'
);
await user.type(
within(screen.getByRole('group', { name: 'Azure Identities' })).getByRole(
'textbox'
),
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/admin'
);
await user.type(
within(
screen.getByRole('group', { name: 'GCP Service Accounts' })
).getByRole('textbox'),
'[email protected]'
);
expect(onChange).toHaveBeenLastCalledWith({
kind: 'app',
labels: [{ name: 'env', value: 'prod' }],
awsRoleARNs: ['arn:aws:iam::123456789012:role/admin'],
azureIdentities: [
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/admin',
],
gcpServiceAccounts: ['[email protected]'],
} as AppAccessSpec);
});

const reactSelectValueContainer = (input: HTMLInputElement) =>
// eslint-disable-next-line testing-library/no-node-access
input.closest('.react-select__value-container');
48 changes: 47 additions & 1 deletion web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ import {
kubernetesResourceKindOptions,
kubernetesVerbOptions,
KubernetesResourceModel,
AppAccessSpec,
} from './standardmodel';
import { EditorSaveCancelButton } from './Shared';
import { RequiresResetToStandard } from './RequiresResetToStandard';
import { FieldMultiInput } from '../../../../shared/components/FieldMultiInput/FieldMultiInput';

export type StandardEditorProps = {
originalRole: RoleWithYaml;
Expand Down Expand Up @@ -356,7 +358,7 @@ const Section = ({
/**
* All access spec kinds, in order of appearance in the resource kind dropdown.
*/
const allAccessSpecKinds: AccessSpecKind[] = ['kube_cluster', 'node'];
const allAccessSpecKinds: AccessSpecKind[] = ['kube_cluster', 'node', 'app'];

/** Maps access specification kind to UI component configuration. */
const specSections: Record<
Expand All @@ -377,6 +379,11 @@ const specSections: Record<
tooltip: 'Configures access to SSH servers',
component: ServerAccessSpecSection,
},
app: {
title: 'Applications',
tooltip: 'Configures access to applications',
component: AppAccessSpecSection,
},
};

/**
Expand Down Expand Up @@ -589,6 +596,45 @@ function KubernetesResourceView({
);
}

export function AppAccessSpecSection({
value,
isProcessing,
onChange,
}: SectionProps<AppAccessSpec>) {
return (
<Flex flexDirection="column" gap={3}>
<Box>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
/>
</Box>
<FieldMultiInput
label="AWS Role ARNs"
disabled={isProcessing}
value={value.awsRoleARNs}
onChange={arns => onChange?.({ ...value, awsRoleARNs: arns })}
/>
<FieldMultiInput
label="Azure Identities"
disabled={isProcessing}
value={value.azureIdentities}
onChange={ids => onChange?.({ ...value, azureIdentities: ids })}
/>
<FieldMultiInput
label="GCP Service Accounts"
disabled={isProcessing}
value={value.gcpServiceAccounts}
onChange={accts => onChange?.({ ...value, gcpServiceAccounts: accts })}
/>
</Flex>
);
}

export const EditorWrapper = styled(Box)<{ mute?: boolean }>`
opacity: ${p => (p.mute ? 0.4 : 1)};
pointer-events: ${p => (p.mute ? 'none' : '')};
Expand Down
117 changes: 117 additions & 0 deletions web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,76 @@ describe('roleToRoleEditorModel', () => {
],
} as RoleEditorModel);
});

it('creates an app access spec', () => {
const minRole = minimalRole();
expect(
roleToRoleEditorModel({
...minRole,
spec: {
...minRole.spec,
allow: {
app_labels: { foo: 'bar' },
},
},
})
).toEqual({
...minimalRoleModel(),
accessSpecs: [
{
kind: 'app',
labels: [{ name: 'foo', value: 'bar' }],
awsRoleARNs: [],
azureIdentities: [],
gcpServiceAccounts: [],
},
],
} as RoleEditorModel);

expect(
roleToRoleEditorModel({
...minRole,
spec: {
...minRole.spec,
allow: {
app_labels: { foo: 'bar' },
aws_role_arns: [
'arn:aws:iam::123456789012:role/role1',
'arn:aws:iam::123456789012:role/role2',
],
azure_identities: [
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id1',
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id2',
],
gcp_service_accounts: [
'[email protected]',
'[email protected]',
],
},
},
})
).toEqual({
...minimalRoleModel(),
accessSpecs: [
{
kind: 'app',
labels: [{ name: 'foo', value: 'bar' }],
awsRoleARNs: [
'arn:aws:iam::123456789012:role/role1',
'arn:aws:iam::123456789012:role/role2',
],
azureIdentities: [
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id1',
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id2',
],
gcpServiceAccounts: [
'[email protected]',
'[email protected]',
],
},
],
} as RoleEditorModel);
});
});

test('labelsToModel', () => {
Expand Down Expand Up @@ -434,6 +504,53 @@ describe('roleEditorModelToRole', () => {
},
} as Role);
});

it('converts an app access spec', () => {
const minRole = minimalRole();
expect(
roleEditorModelToRole({
...minimalRoleModel(),
accessSpecs: [
{
kind: 'app',
labels: [{ name: 'foo', value: 'bar' }],
awsRoleARNs: [
'arn:aws:iam::123456789012:role/role1',
'arn:aws:iam::123456789012:role/role2',
],
azureIdentities: [
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id1',
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id2',
],
gcpServiceAccounts: [
'[email protected]',
'[email protected]',
],
},
],
})
).toEqual({
...minRole,
spec: {
...minRole.spec,
allow: {
app_labels: { foo: 'bar' },
aws_role_arns: [
'arn:aws:iam::123456789012:role/role1',
'arn:aws:iam::123456789012:role/role2',
],
azure_identities: [
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id1',
'/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id2',
],
gcp_service_accounts: [
'[email protected]',
'[email protected]',
],
},
},
} as Role);
});
});

test('labelsModelToLabels', () => {
Expand Down
Loading

0 comments on commit 64a67e3

Please sign in to comment.