Skip to content

Commit

Permalink
Add database and Windows access sections to the role editor (#47992)
Browse files Browse the repository at this point in the history
* Add database and Windows access sections to the role editor

* review

* Fix ta typo
  • Loading branch information
bl-nero committed Nov 19, 2024
1 parent ce71412 commit 0e122bf
Show file tree
Hide file tree
Showing 6 changed files with 495 additions and 202 deletions.
3 changes: 3 additions & 0 deletions web/packages/shared/components/FieldSelect/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export function splitSelectProps<
onChange,
onInputChange,
onKeyDown,
openMenuOnClick,
options,
placeholder,
rule,
Expand Down Expand Up @@ -239,6 +240,7 @@ export function splitSelectProps<
onInputChange,
onKeyDown,
options,
openMenuOnClick,
placeholder,
stylesConfig,
value,
Expand Down Expand Up @@ -285,6 +287,7 @@ type KeysRemovedFromOthers =
| 'onChange'
| 'onInputChange'
| 'onKeyDown'
| 'openMenuOnClick'
| 'options'
| 'placeholder'
| 'rule'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,24 @@ import { createTeleportContext } from 'teleport/mocks/contexts';
import {
AccessSpec,
AppAccessSpec,
DatabaseAccessSpec,
KubernetesAccessSpec,
newAccessSpec,
newRole,
roleToRoleEditorModel,
ServerAccessSpec,
StandardEditorModel,
WindowsDesktopAccessSpec,
} from './standardmodel';
import {
AppAccessSpecSection,
DatabaseAccessSpecSection,
KubernetesAccessSpecSection,
SectionProps,
ServerAccessSpecSection,
StandardEditor,
StandardEditorProps,
WindowsDesktopAccessSpecSection,
} from './StandardEditor';

const TestStandardEditor = (props: Partial<StandardEditorProps>) => {
Expand Down Expand Up @@ -76,6 +80,8 @@ test('adding and removing sections', async () => {
'Kubernetes',
'Servers',
'Applications',
'Databases',
'Windows Desktops',
]);

await user.click(screen.getByRole('menuitem', { name: 'Servers' }));
Expand All @@ -84,7 +90,12 @@ test('adding and removing sections', async () => {
await user.click(
screen.getByRole('button', { name: 'Add New Specifications' })
);
expect(getAllMenuItemNames()).toEqual(['Kubernetes', 'Applications']);
expect(getAllMenuItemNames()).toEqual([
'Kubernetes',
'Applications',
'Databases',
'Windows Desktops',
]);

await user.click(screen.getByRole('menuitem', { name: 'Kubernetes' }));
expect(getAllSectionNames()).toEqual([
Expand Down Expand Up @@ -353,6 +364,62 @@ test('AppAccessSpecSection', async () => {
} as AppAccessSpec);
});

test('DatabaseAccessSpecSection', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<StatefulSection<DatabaseAccessSpec>
component={DatabaseAccessSpecSection}
defaultValue={newAccessSpec('db')}
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 selectEvent.create(screen.getByLabelText('Database Names'), 'stuff', {
createOptionText: 'Database Name: stuff',
});
await selectEvent.create(screen.getByLabelText('Database Users'), 'mary', {
createOptionText: 'Database User: mary',
});
await selectEvent.create(screen.getByLabelText('Database Roles'), 'admin', {
createOptionText: 'Database Role: admin',
});
expect(onChange).toHaveBeenLastCalledWith({
kind: 'db',
labels: [{ name: 'env', value: 'prod' }],
names: [expect.objectContaining({ label: 'stuff', value: 'stuff' })],
roles: [expect.objectContaining({ label: 'admin', value: 'admin' })],
users: [expect.objectContaining({ label: 'mary', value: 'mary' })],
} as DatabaseAccessSpec);
});

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

await user.click(screen.getByRole('button', { name: 'Add a Label' }));
await user.type(screen.getByPlaceholderText('label key'), 'os');
await user.type(screen.getByPlaceholderText('label value'), 'win-xp');
await selectEvent.create(screen.getByLabelText('Logins'), 'julio', {
createOptionText: 'Login: julio',
});
expect(onChange).toHaveBeenLastCalledWith({
kind: 'windows_desktop',
labels: [{ name: 'os', value: 'win-xp' }],
logins: [expect.objectContaining({ label: 'julio', value: 'julio' })],
} as WindowsDesktopAccessSpec);
});

const reactSelectValueContainer = (input: HTMLInputElement) =>
// eslint-disable-next-line testing-library/no-node-access
input.closest('.react-select__value-container');
127 changes: 126 additions & 1 deletion web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import {
kubernetesVerbOptions,
KubernetesResourceModel,
AppAccessSpec,
DatabaseAccessSpec,
WindowsDesktopAccessSpec,
} from './standardmodel';
import { EditorSaveCancelButton } from './Shared';
import { RequiresResetToStandard } from './RequiresResetToStandard';
Expand Down Expand Up @@ -359,7 +361,13 @@ const Section = ({
/**
* All access spec kinds, in order of appearance in the resource kind dropdown.
*/
const allAccessSpecKinds: AccessSpecKind[] = ['kube_cluster', 'node', 'app'];
const allAccessSpecKinds: AccessSpecKind[] = [
'kube_cluster',
'node',
'app',
'db',
'windows_desktop',
];

/** Maps access specification kind to UI component configuration. */
const specSections: Record<
Expand All @@ -385,6 +393,16 @@ const specSections: Record<
tooltip: 'Configures access to applications',
component: AppAccessSpecSection,
},
db: {
title: 'Databases',
tooltip: 'Configures access to databases',
component: DatabaseAccessSpecSection,
},
windows_desktop: {
title: 'Windows Desktops',
tooltip: 'Configures access to Windows desktops',
component: WindowsDesktopAccessSpecSection,
},
};

/**
Expand Down Expand Up @@ -436,6 +454,7 @@ export function ServerAccessSpecSection({
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.logins}
onChange={logins => onChange?.({ ...value, logins })}
mt={3}
Expand All @@ -460,6 +479,7 @@ export function KubernetesAccessSpecSection({
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.groups}
onChange={groups => onChange?.({ ...value, groups })}
/>
Expand Down Expand Up @@ -636,6 +656,111 @@ export function AppAccessSpecSection({
);
}

export function DatabaseAccessSpecSection({
value,
isProcessing,
onChange,
}: SectionProps<DatabaseAccessSpec>) {
return (
<>
<Box mb={3}>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
/>
</Box>
<FieldSelectCreatable
isMulti
label="Database Names"
toolTipContent={
<>
List of database names that this role is allowed to connect to.
Special value <MarkInverse>*</MarkInverse> means any name.
</>
}
isDisabled={isProcessing}
formatCreateLabel={label => `Database Name: ${label}`}
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.names}
onChange={names => onChange?.({ ...value, names })}
/>
<FieldSelectCreatable
isMulti
label="Database Users"
toolTipContent={
<>
List of database users that this role is allowed to connect as.
Special value <MarkInverse>*</MarkInverse> means any user.
</>
}
isDisabled={isProcessing}
formatCreateLabel={label => `Database User: ${label}`}
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.users}
onChange={users => onChange?.({ ...value, users })}
/>
<FieldSelectCreatable
isMulti
label="Database Roles"
toolTipContent="If automatic user provisioning is available, this is the list of database roles that will be assigned to the database user after it's created"
isDisabled={isProcessing}
formatCreateLabel={label => `Database Role: ${label}`}
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.roles}
onChange={roles => onChange?.({ ...value, roles })}
mb={0}
/>
</>
);
}

export function WindowsDesktopAccessSpecSection({
value,
isProcessing,
onChange,
}: SectionProps<WindowsDesktopAccessSpec>) {
return (
<>
<Box mb={3}>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
/>
</Box>
<FieldSelectCreatable
isMulti
label="Logins"
toolTipContent="List of desktop logins that this role is allowed to use"
isDisabled={isProcessing}
formatCreateLabel={label => `Login: ${label}`}
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.logins}
onChange={logins => onChange?.({ ...value, logins })}
/>
</>
);
}

export const EditorWrapper = styled(Box)<{ mute?: boolean }>`
opacity: ${p => (p.mute ? 0.4 : 1)};
pointer-events: ${p => (p.mute ? 'none' : '')};
Expand Down
Loading

0 comments on commit 0e122bf

Please sign in to comment.