Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support more fields to the role editor #51458

Open
wants to merge 1 commit into
base: bl-nero/fieldinput-labelsinput
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions lib/services/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ func NewSystemAutomaticAccessBotUser() types.User {
// NewPresetEditorRole returns a new pre-defined role for cluster
// editors who can edit cluster configuration resources.
func NewPresetEditorRole() types.Role {
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
role := &types.RoleV6{
Kind: types.KindRole,
Version: types.V7,
Expand All @@ -116,6 +120,10 @@ func NewPresetEditorRole() types.Role {
},
},
Spec: types.RoleSpecV6{
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
Options: types.RoleOptions{
CertificateFormat: constants.CertificateFormatStandard,
MaxSessionTTL: types.NewDuration(apidefaults.MaxCertDuration),
Expand All @@ -133,6 +141,10 @@ func NewPresetEditorRole() types.Role {
Desktop: types.NewBoolOption(false),
},
},
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
Allow: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
Rules: []types.Rule{
Expand Down Expand Up @@ -208,6 +220,10 @@ func NewPresetEditorRole() types.Role {
// NewPresetAccessRole creates a role for users who are allowed to initiate
// interactive sessions.
func NewPresetAccessRole() types.Role {
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
role := &types.RoleV6{
Kind: types.KindRole,
Version: types.V7,
Expand All @@ -220,6 +236,10 @@ func NewPresetAccessRole() types.Role {
},
},
Spec: types.RoleSpecV6{
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
Options: types.RoleOptions{
CertificateFormat: constants.CertificateFormatStandard,
MaxSessionTTL: types.NewDuration(apidefaults.MaxCertDuration),
Expand All @@ -235,6 +255,10 @@ func NewPresetAccessRole() types.Role {
BPF: apidefaults.EnhancedEvents(),
RecordSession: &types.RecordSession{Desktop: types.NewBoolOption(true)},
},
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
Allow: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
Expand Down Expand Up @@ -270,6 +294,10 @@ func NewPresetAccessRole() types.Role {
},
},
}
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
role.SetLogins(types.Allow, []string{teleport.TraitInternalLoginsVariable})
role.SetWindowsLogins(types.Allow, []string{teleport.TraitInternalWindowsLoginsVariable})
role.SetKubeUsers(types.Allow, []string{teleport.TraitInternalKubeUsersVariable})
Expand All @@ -284,6 +312,10 @@ func NewPresetAccessRole() types.Role {
// auditor - someone who can review cluster events and replay sessions,
// but can't initiate interactive sessions or modify configuration.
func NewPresetAuditorRole() types.Role {
// IMPORTANT: Before adding new defaults, please make sure that the
// underlying field is supported by the standard role editor UI. This role
// should be editable with a rich UI, without requiring the user to dive into
// YAML.
role := &types.RoleV6{
Kind: types.KindRole,
Version: types.V7,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('AccessRules', () => {
'list',
'read',
]);
await user.type(screen.getByLabelText('Condition'), 'some-condition');
expect(modelRef).toHaveBeenLastCalledWith([
{
id: expect.any(String),
Expand All @@ -69,6 +70,7 @@ describe('AccessRules', () => {
{ label: 'list', value: 'list' },
{ label: 'read', value: 'read' },
],
where: 'some-condition',
},
] as RuleModel[]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@

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

import { ButtonSecondary } from 'design/Button';
import Flex from 'design/Flex';
import { Plus } from 'design/Icon';
import Text from 'design/Text';
import { HoverTooltip } from 'design/Tooltip';
import FieldInput from 'shared/components/FieldInput';
import {
FieldSelect,
FieldSelectCreatable,
Expand Down Expand Up @@ -78,7 +80,8 @@ const AccessRule = memo(function AccessRule({
validation,
dispatch,
}: SectionPropsWithDispatch<RuleModel, AccessRuleValidationResult>) {
const { id, resources, verbs } = value;
const { id, resources, verbs, where } = value;
const theme = useTheme();
function setRule(rule: RuleModel) {
dispatch({ type: 'set-access-rule', payload: rule });
}
Expand Down Expand Up @@ -112,6 +115,26 @@ const AccessRule = memo(function AccessRule({
value={verbs}
onChange={v => setRule({ ...value, verbs: v })}
rule={precomputed(validation.fields.verbs)}
/>
<FieldInput
label="Condition"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally i think it's okay to label it this way, but i'd also double check with product team because we try to teach users consistent wording

toolTipContent={
<>
Additional condition, expressed using the{' '}
<Text
as="a"
href="https://goteleport.com/docs/reference/predicate-language/"
target="_blank"
color={theme.colors.interactive.solid.accent.default}
>
Teleport predicate language
</Text>
Comment on lines +123 to +131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about something like: Optional condition further limiting the scope for this rule expressed using the ....

</>
}
tooltipSticky
disabled={isProcessing}
value={where}
onChange={e => setRule({ ...value, where: e.target.value })}
mb={0}
/>
</SectionBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ describe('KubernetesAccessSection', () => {
await user.type(screen.getByPlaceholderText('label key'), 'some-key');
await user.type(screen.getByPlaceholderText('label value'), 'some-value');

await selectEvent.create(screen.getByLabelText('Users'), 'joe', {
createOptionText: 'User: joe',
});
await selectEvent.create(screen.getByLabelText('Users'), 'mary', {
createOptionText: 'User: mary',
});

await user.click(screen.getByRole('button', { name: 'Add a Resource' }));
expect(
reactSelectValueContainer(screen.getByLabelText('Kind'))
Expand Down Expand Up @@ -178,6 +185,10 @@ describe('KubernetesAccessSection', () => {
roleVersion: 'v7',
},
],
users: [
expect.objectContaining({ value: 'joe' }),
expect.objectContaining({ value: 'mary' }),
],
roleVersion: 'v7',
} as KubernetesAccess);
});
Expand Down Expand Up @@ -391,9 +402,12 @@ describe('DatabaseAccessSection', () => {

test('editing', async () => {
const { user, onChange } = setup();
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');

const labels = within(screen.getByRole('group', { name: 'Labels' }));
await user.click(labels.getByRole('button', { name: 'Add a Label' }));
await user.type(labels.getByPlaceholderText('label key'), 'env');
await user.type(labels.getByPlaceholderText('label value'), 'prod');

await selectEvent.create(screen.getByLabelText('Database Names'), 'stuff', {
createOptionText: 'Database Name: stuff',
});
Expand All @@ -403,6 +417,16 @@ describe('DatabaseAccessSection', () => {
await selectEvent.create(screen.getByLabelText('Database Roles'), 'admin', {
createOptionText: 'Database Role: admin',
});

const dbServiceLabels = within(
screen.getByRole('group', { name: 'Database Service Labels' })
);
await user.click(
dbServiceLabels.getByRole('button', { name: 'Add a Label' })
);
await user.type(dbServiceLabels.getByPlaceholderText('label key'), 'foo');
await user.type(dbServiceLabels.getByPlaceholderText('label value'), 'bar');

expect(onChange).toHaveBeenLastCalledWith({
kind: 'db',
labels: [{ name: 'env', value: 'prod' }],
Expand All @@ -418,18 +442,29 @@ describe('DatabaseAccessSection', () => {
expect.objectContaining({ value: '{{internal.db_users}}' }),
expect.objectContaining({ label: 'mary', value: 'mary' }),
],
dbServiceLabels: [{ name: 'foo', value: 'bar' }],
} as DatabaseAccess);
});

test('validation', async () => {
const { user, validator } = setup();
await user.click(screen.getByRole('button', { name: 'Add a Label' }));
const labels = within(screen.getByRole('group', { name: 'Labels' }));
await user.click(labels.getByRole('button', { name: 'Add a Label' }));
const dbServiceLabelsGroup = within(
screen.getByRole('group', { name: 'Database Service Labels' })
);
await user.click(
dbServiceLabelsGroup.getByRole('button', { name: 'Add a Label' })
);
await selectEvent.create(screen.getByLabelText('Database Roles'), '*', {
createOptionText: 'Database Role: *',
});
act(() => validator.validate());
expect(
screen.getByPlaceholderText('label key')
labels.getByPlaceholderText('label key')
).toHaveAccessibleDescription('required');
expect(
dbServiceLabelsGroup.getByPlaceholderText('label key')
).toHaveAccessibleDescription('required');
expect(
screen.getByText('Wildcard is not allowed in database roles')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import ButtonIcon from 'design/ButtonIcon';
import Flex from 'design/Flex';
import { Add, Plus, Trash } from 'design/Icon';
import { Mark } from 'design/Mark';
import Text, { H4 } from 'design/Text';
import { H4 } from 'design/Text';
import FieldInput from 'shared/components/FieldInput';
import { FieldMultiInput } from 'shared/components/FieldMultiInput/FieldMultiInput';
import {
Expand Down Expand Up @@ -232,10 +232,8 @@ export function ServerAccessSection({
}: SectionProps<ServerAccess, ServerAccessValidationResult>) {
return (
<>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
legend="Labels"
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
Expand Down Expand Up @@ -281,10 +279,21 @@ export function KubernetesAccessSection({
onChange={groups => onChange?.({ ...value, groups })}
/>

<Text typography="body3" mb={1}>
Labels
</Text>
<FieldSelectCreatable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the inputs placeholder says Select..., are we supposed to have a list of users to select? or maybe replace with Start typing a user and press enter?

isMulti
label="Users"
isDisabled={isProcessing}
formatCreateLabel={label => `User: ${label}`}
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.users}
onChange={users => onChange?.({ ...value, users })}
/>

<LabelsInput
legend="Labels"
disableBtns={isProcessing}
labels={value.labels}
rule={precomputed(validation.fields.labels)}
Expand Down Expand Up @@ -433,17 +442,13 @@ export function AppAccessSection({
}: SectionProps<AppAccess, AppAccessValidationResult>) {
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 })}
rule={precomputed(validation.fields.labels)}
/>
</Box>
<LabelsInput
legend="Labels"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also add a tooltip explaining labels as well?

disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
rule={precomputed(validation.fields.labels)}
/>
<FieldMultiInput
label="AWS Role ARNs"
disabled={isProcessing}
Expand Down Expand Up @@ -478,10 +483,8 @@ export function DatabaseAccessSection({
return (
<>
<Box mb={3}>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
legend="Labels"
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
Expand Down Expand Up @@ -537,7 +540,14 @@ export function DatabaseAccessSection({
value={value.roles}
onChange={roles => onChange?.({ ...value, roles })}
rule={precomputed(validation.fields.roles)}
mb={0}
/>
<LabelsInput
legend="Database Service Labels"
tooltipContent="The database service labels have no influence on which databases' access is controlled by this role. Instead, they control which database services are discoverable while enrolling a new database."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, they control which database services are discoverable while enrolling a new database.

tbh, i didn't understand what this means 😢, can we simplify it? The ai doc says:

image

disableBtns={isProcessing}
labels={value.dbServiceLabels}
setLabels={dbServiceLabels => onChange?.({ ...value, dbServiceLabels })}
rule={precomputed(validation.fields.dbServiceLabels)}
/>
</>
);
Expand All @@ -552,10 +562,8 @@ export function WindowsDesktopAccessSection({
return (
<>
<Box mb={3}>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
legend="Labels"
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
Expand Down
Loading
Loading