Skip to content

Commit

Permalink
Add more fields to the standard role editor (#50067)
Browse files Browse the repository at this point in the history
Also relax some rules about the resource kind names in access rules.
  • Loading branch information
bl-nero authored Dec 13, 2024
1 parent aa69202 commit ea1ee3d
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 47 deletions.
96 changes: 92 additions & 4 deletions web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ import {
FieldSelectCreatable,
} from 'shared/components/FieldSelect';
import { SlideTabs } from 'design/SlideTabs';

import { RadioGroup } from 'design/RadioGroup';

import Select from 'shared/components/Select';

import { components, MultiValueProps } from 'react-select';

import { Role, RoleWithYaml } from 'teleport/services/resources';
import { LabelsInput } from 'teleport/components/LabelsInput';

Expand Down Expand Up @@ -79,6 +79,10 @@ import {
OptionsModel,
requireMFATypeOptions,
createHostUserModeOptions,
createDBUserModeOptions,
sessionRecordingModeOptions,
resourceKindOptionsMap,
ResourceKindOption,
} from './standardmodel';
import {
validateRoleEditorModel,
Expand Down Expand Up @@ -424,11 +428,19 @@ const MetadataSection = ({
placeholder="Enter Role Description"
value={value.description || ''}
disabled={isProcessing}
mb={0}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange({ ...value, description: e.target.value })
}
/>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
rule={precomputed(validation.fields.labels)}
/>
</Section>
);

Expand Down Expand Up @@ -1014,7 +1026,8 @@ function AccessRule({
validation={validation}
onRemove={onRemove}
>
<FieldSelect
<ResourceKindSelect
components={{ MultiValue: ResourceKindMultiValue }}
isMulti
label="Resources"
isDisabled={isProcessing}
Expand All @@ -1037,6 +1050,32 @@ function AccessRule({
);
}

const ResourceKindSelect = styled(
FieldSelectCreatable<ResourceKindOption, true>
)`
.teleport-resourcekind__value--unknown {
background: ${props => props.theme.colors.interactive.solid.alert.default};
.react-select__multi-value__label,
.react-select__multi-value__remove {
color: ${props => props.theme.colors.text.primaryInverse};
}
}
`;

function ResourceKindMultiValue(props: MultiValueProps<ResourceKindOption>) {
if (resourceKindOptionsMap.has(props.data.value)) {
return <components.MultiValue {...props} />;
}
return (
<HoverTooltip tipContent="Unrecognized resource type">
<components.MultiValue
{...props}
className="teleport-resourcekind__value--unknown"
/>
</HoverTooltip>
);
}

function Options({
value,
isProcessing,
Expand All @@ -1048,6 +1087,9 @@ function Options({
const clientIdleTimeoutId = `${id}-client-idle-timeout`;
const requireMFATypeId = `${id}-require-mfa-type`;
const createHostUserModeId = `${id}-create-host-user-mode`;
const createDBUserModeId = `${id}-create-db-user-mode`;
const defaultSessionRecordingModeId = `${id}-default-session-recording-mode`;
const sshSessionRecordingModeId = `${id}-ssh-session-recording-mode`;
return (
<OptionsGridContainer
border={1}
Expand Down Expand Up @@ -1093,6 +1135,17 @@ function Options({
onChange={t => onChange?.({ ...value, requireMFAType: t })}
/>

<OptionLabel htmlFor={defaultSessionRecordingModeId}>
Default Session Recording Mode
</OptionLabel>
<Select
inputId={defaultSessionRecordingModeId}
isDisabled={isProcessing}
options={sessionRecordingModeOptions}
value={value.defaultSessionRecordingMode}
onChange={m => onChange?.({ ...value, defaultSessionRecordingMode: m })}
/>

<OptionsHeader separator>SSH</OptionsHeader>

<OptionLabel htmlFor={createHostUserModeId}>
Expand All @@ -1106,6 +1159,24 @@ function Options({
onChange={m => onChange?.({ ...value, createHostUserMode: m })}
/>

<Box>Agent Forwarding</Box>
<BoolRadioGroup
name="forward-agent"
value={value.forwardAgent}
onChange={f => onChange({ ...value, forwardAgent: f })}
/>

<OptionLabel htmlFor={sshSessionRecordingModeId}>
Session Recording Mode
</OptionLabel>
<Select
inputId={sshSessionRecordingModeId}
isDisabled={isProcessing}
options={sessionRecordingModeOptions}
value={value.sshSessionRecordingMode}
onChange={m => onChange?.({ ...value, sshSessionRecordingMode: m })}
/>

<OptionsHeader separator>Database</OptionsHeader>

<Box>Create Database User</Box>
Expand All @@ -1117,6 +1188,16 @@ function Options({

{/* TODO(bl-nero): a bug in YAML unmarshalling backend breaks the
createDBUserMode field. Fix it and add the field here. */}
<OptionLabel htmlFor={createDBUserModeId}>
Create Database User Mode
</OptionLabel>
<Select
inputId={createDBUserModeId}
isDisabled={isProcessing}
options={createDBUserModeOptions}
value={value.createDBUserMode}
onChange={m => onChange?.({ ...value, createDBUserMode: m })}
/>

<OptionsHeader separator>Desktop</OptionsHeader>

Expand All @@ -1140,6 +1221,13 @@ function Options({
value={value.desktopDirectorySharing}
onChange={s => onChange({ ...value, desktopDirectorySharing: s })}
/>

<Box>Record Desktop Sessions</Box>
<BoolRadioGroup
name="record-desktop-sessions"
value={value.recordDesktopSessions}
onChange={r => onChange({ ...value, recordDesktopSessions: r })}
/>
</OptionsGridContainer>
);
}
Expand Down
87 changes: 63 additions & 24 deletions web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ResourceKind,
Role,
Rule,
SessionRecordingMode,
} from 'teleport/services/resources';

import { Label as UILabel } from 'teleport/components/LabelsInput/LabelsInput';
Expand All @@ -38,13 +39,13 @@ import {
roleEditorModelToRole,
roleToRoleEditorModel,
} from './standardmodel';
import { withDefaults } from './withDefaults';
import { optionsWithDefaults, withDefaults } from './withDefaults';

const minimalRole = () =>
withDefaults({ metadata: { name: 'foobar' }, version: 'v7' });

const minimalRoleModel = (): RoleEditorModel => ({
metadata: { name: 'foobar' },
metadata: { name: 'foobar', labels: [] },
accessSpecs: [],
rules: [],
requiresReset: false,
Expand All @@ -68,6 +69,10 @@ const minimalRoleModel = (): RoleEditorModel => ({
desktopClipboard: true,
createDesktopUser: false,
desktopDirectorySharing: true,
defaultSessionRecordingMode: { value: 'best_effort', label: 'Best Effort' },
sshSessionRecordingMode: { value: '', label: 'Unspecified' },
recordDesktopSessions: true,
forwardAgent: false,
},
});

Expand All @@ -83,13 +88,15 @@ describe.each<{ name: string; role: Role; model: RoleEditorModel }>([
metadata: {
name: 'role-name',
description: 'role-description',
labels: { foo: 'bar' },
},
},
model: {
...minimalRoleModel(),
metadata: {
name: 'role-name',
description: 'role-description',
labels: [{ name: 'foo', value: 'bar' }],
},
},
},
Expand Down Expand Up @@ -250,6 +257,12 @@ describe.each<{ name: string; role: Role; model: RoleEditorModel }>([
desktop_clipboard: false,
create_desktop_user: true,
desktop_directory_sharing: false,
record_session: {
default: 'strict',
desktop: false,
ssh: 'best_effort',
},
forward_agent: true,
},
},
},
Expand All @@ -269,6 +282,10 @@ describe.each<{ name: string; role: Role; model: RoleEditorModel }>([
desktopClipboard: false,
createDesktopUser: true,
desktopDirectorySharing: false,
defaultSessionRecordingMode: { value: 'strict', label: 'Strict' },
sshSessionRecordingMode: { value: 'best_effort', label: 'Best Effort' },
recordDesktopSessions: false,
forwardAgent: true,
},
},
},
Expand Down Expand Up @@ -425,28 +442,6 @@ describe('roleToRoleEditorModel', () => {
},
},

{
name: 'unsupported resource kind in Rule',
role: {
...minRole,
spec: {
...minRole.spec,
allow: {
...minRole.spec.allow,
rules: [{ resources: ['illegal', 'node'] } as unknown as Rule],
},
},
} as Role,
model: {
...roleModelWithReset,
rules: [
expect.objectContaining({
resources: [{ value: 'node', label: 'node' }],
}),
],
},
},

{
name: 'unsupported verb in Rule',
role: {
Expand Down Expand Up @@ -584,6 +579,46 @@ describe('roleToRoleEditorModel', () => {
},
},
},

{
name: 'unsupported value in spec.options.record_session.default',
role: {
...minRole,
spec: {
...minRole.spec,
options: optionsWithDefaults({
record_session: { default: 'bogus' as SessionRecordingMode },
}),
},
},
model: {
...roleModelWithReset,
options: {
...roleModelWithReset.options,
defaultSessionRecordingMode: { value: '', label: 'Unspecified' },
},
},
},

{
name: 'unsupported value in spec.options.record_session.ssh',
role: {
...minRole,
spec: {
...minRole.spec,
options: optionsWithDefaults({
record_session: { ssh: 'bogus' as SessionRecordingMode },
}),
},
},
model: {
...roleModelWithReset,
options: {
...roleModelWithReset.options,
sshSessionRecordingMode: { value: '', label: 'Unspecified' },
},
},
},
])(
'requires reset because of $name',
({ role, model = roleModelWithReset }) => {
Expand Down Expand Up @@ -618,6 +653,7 @@ describe('roleToRoleEditorModel', () => {
metadata: {
name: 'role-name',
revision: originalRev,
labels: [],
},
requiresReset: true,
} as RoleEditorModel);
Expand Down Expand Up @@ -646,6 +682,7 @@ describe('roleToRoleEditorModel', () => {
metadata: {
name: 'role-name',
revision: 'e39ea9f1-79b7-4d28-8f0c-af6848f9e655',
labels: [],
},
requiresReset: true,
} as RoleEditorModel);
Expand Down Expand Up @@ -798,6 +835,7 @@ describe('roleEditorModelToRole', () => {
name: 'dog-walker',
description: 'walks dogs',
revision: 'e2a3ccf8-09b9-4d97-8e47-6dbe3d53c0e5',
labels: [{ name: 'kind', value: 'occupation' }],
},
})
).toEqual({
Expand All @@ -806,6 +844,7 @@ describe('roleEditorModelToRole', () => {
name: 'dog-walker',
description: 'walks dogs',
revision: 'e2a3ccf8-09b9-4d97-8e47-6dbe3d53c0e5',
labels: { kind: 'occupation' },
},
} as Role);
});
Expand Down
Loading

0 comments on commit ea1ee3d

Please sign in to comment.