mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Add support for Linux desktop labels and logins in role editor (#68048)
* Add support for Linux desktop labels and logins in role editor * test fix
This commit is contained in:
@@ -644,6 +644,10 @@ const (
|
||||
// allowed Windows Desktop logins for local accounts.
|
||||
TraitInternalWindowsLoginsVariable = "{{internal.windows_logins}}"
|
||||
|
||||
// TraitInternalLinuxDesktopLoginsVariable is the variable used to store
|
||||
// allowed Linux Desktop logins for local accounts.
|
||||
TraitInternalLinuxDesktopLoginsVariable = "{{internal.linux_desktop_logins}}"
|
||||
|
||||
// TraitInternalKubeGroupsVariable is the variable used to store allowed
|
||||
// kubernetes groups for local accounts.
|
||||
TraitInternalKubeGroupsVariable = "{{internal.kubernetes_groups}}"
|
||||
|
||||
@@ -150,6 +150,12 @@
|
||||
"tools": [
|
||||
"{{internal.mcp_tools}}"
|
||||
]
|
||||
},
|
||||
"linux_desktop_logins": [
|
||||
"{{internal.linux_desktop_logins}}"
|
||||
],
|
||||
"linux_desktop_labels": {
|
||||
"*": "*"
|
||||
}
|
||||
},
|
||||
"deny": {}
|
||||
|
||||
@@ -291,6 +291,7 @@ func NewPresetAccessRole() types.Role {
|
||||
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
||||
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
||||
WindowsDesktopLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
||||
LinuxDesktopLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
||||
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
||||
DatabaseServiceLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
|
||||
DatabaseNames: []string{teleport.TraitInternalDBNamesVariable},
|
||||
@@ -330,6 +331,7 @@ func NewPresetAccessRole() types.Role {
|
||||
// YAML.
|
||||
role.SetLogins(types.Allow, []string{teleport.TraitInternalLoginsVariable})
|
||||
role.SetWindowsLogins(types.Allow, []string{teleport.TraitInternalWindowsLoginsVariable})
|
||||
role.SetLinuxDesktopLogins(types.Allow, []string{teleport.TraitInternalLinuxDesktopLoginsVariable})
|
||||
role.SetKubeUsers(types.Allow, []string{teleport.TraitInternalKubeUsersVariable})
|
||||
role.SetKubeGroups(types.Allow, []string{teleport.TraitInternalKubeGroupsVariable})
|
||||
role.SetAWSRoleARNs(types.Allow, []string{teleport.TraitInternalAWSRoleARNs})
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
DatabaseAccessSection,
|
||||
GitHubOrganizationAccessSection,
|
||||
KubernetesAccessSection,
|
||||
LinuxDesktopAccessSection,
|
||||
ServerAccessSection,
|
||||
WindowsDesktopAccessSection,
|
||||
} from './Resources';
|
||||
@@ -55,6 +56,7 @@ const meta: Meta<StoryProps> = {
|
||||
'app',
|
||||
'db',
|
||||
'windows_desktop',
|
||||
'linux_desktop',
|
||||
'git_server',
|
||||
],
|
||||
},
|
||||
@@ -146,6 +148,12 @@ function getResourceSectionStates(kind: ResourceAccessKind) {
|
||||
defaultValue: newResourceAccess('windows_desktop', defaultRoleVersion),
|
||||
};
|
||||
|
||||
case 'linux_desktop':
|
||||
return {
|
||||
component: LinuxDesktopAccessSection,
|
||||
defaultValue: newResourceAccess('linux_desktop', defaultRoleVersion),
|
||||
};
|
||||
|
||||
default:
|
||||
kind satisfies never;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
DatabaseAccessSection,
|
||||
GitHubOrganizationAccessSection,
|
||||
KubernetesAccessSection,
|
||||
LinuxDesktopAccessSection,
|
||||
ServerAccessSection,
|
||||
WindowsDesktopAccessSection,
|
||||
} from './Resources';
|
||||
@@ -42,6 +43,8 @@ import {
|
||||
GitHubOrganizationAccessInputFields,
|
||||
KubernetesAccess,
|
||||
KubernetesAccessInputFields,
|
||||
LinuxDesktopAccess,
|
||||
LinuxDesktopAccessInputFields,
|
||||
newResourceAccess,
|
||||
ServerAccess,
|
||||
ServerAccessInputFields,
|
||||
@@ -724,6 +727,73 @@ describe('WindowsDesktopAccessSection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinuxDesktopAccessSection', () => {
|
||||
const setup = (visibleInputFields?: LinuxDesktopAccessInputFields) => {
|
||||
const onChange = jest.fn();
|
||||
let validator: Validator;
|
||||
render(
|
||||
<StatefulSection<
|
||||
LinuxDesktopAccess,
|
||||
ResourceAccessValidationResult,
|
||||
LinuxDesktopAccessInputFields
|
||||
>
|
||||
component={LinuxDesktopAccessSection}
|
||||
defaultValue={newResourceAccess('linux_desktop', defaultRoleVersion)}
|
||||
onChange={onChange}
|
||||
validatorRef={v => {
|
||||
validator = v;
|
||||
}}
|
||||
validate={validateResourceAccess}
|
||||
visibleInputFields={visibleInputFields}
|
||||
/>
|
||||
);
|
||||
return { user: userEvent.setup(), onChange, validator };
|
||||
};
|
||||
|
||||
test('editing', async () => {
|
||||
const { user, onChange } = setup();
|
||||
await user.type(screen.getByPlaceholderText('label key'), 'os');
|
||||
await user.type(screen.getByPlaceholderText('label value'), 'ubuntu');
|
||||
await selectEvent.create(screen.getByLabelText('Logins'), 'alice', {
|
||||
createOptionText: 'Login: alice',
|
||||
});
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
kind: 'linux_desktop',
|
||||
labels: [{ name: 'os', value: 'ubuntu' }],
|
||||
logins: [
|
||||
expect.objectContaining({ value: '{{internal.logins}}' }),
|
||||
expect.objectContaining({ label: 'alice', value: 'alice' }),
|
||||
],
|
||||
hideValidationErrors: true,
|
||||
} as LinuxDesktopAccess);
|
||||
});
|
||||
|
||||
test('validation', async () => {
|
||||
const { user, validator } = setup();
|
||||
await user.type(screen.getByPlaceholderText('label value'), 'some-value');
|
||||
act(() => validator.validate());
|
||||
expect(
|
||||
screen.getByPlaceholderText('label key')
|
||||
).toHaveAccessibleDescription('required');
|
||||
});
|
||||
|
||||
test('hide all input fields', async () => {
|
||||
setup({ labels: false, logins: false });
|
||||
expect(screen.queryByPlaceholderText('label key')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByPlaceholderText('label value')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/logins/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hide one input field', async () => {
|
||||
setup({ labels: true, logins: false });
|
||||
expect(screen.getByPlaceholderText('label key')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('label value')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/logins/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitHubOrganizationAccessSection', () => {
|
||||
const setup = (visibleInputFields?: GitHubOrganizationAccessInputFields) => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
@@ -61,6 +61,8 @@ import {
|
||||
KubernetesResourceModel,
|
||||
kubernetesVerbOptions,
|
||||
newKubernetesResourceModel,
|
||||
LinuxDesktopAccess,
|
||||
LinuxDesktopAccessInputFields,
|
||||
ResourceAccess,
|
||||
ResourceAccessKind,
|
||||
ServerAccess,
|
||||
@@ -76,6 +78,7 @@ import {
|
||||
GitHubOrganizationAccessValidationResult,
|
||||
KubernetesAccessValidationResult,
|
||||
KubernetesResourceValidationResult,
|
||||
LinuxDesktopAccessValidationResult,
|
||||
ResourceAccessValidationResult,
|
||||
ServerAccessValidationResult,
|
||||
v7kubernetesClusterWideResourceKinds,
|
||||
@@ -174,6 +177,7 @@ const allResourceAccessKinds: ResourceAccessKind[] = [
|
||||
'app',
|
||||
'db',
|
||||
'windows_desktop',
|
||||
'linux_desktop',
|
||||
'git_server',
|
||||
];
|
||||
|
||||
@@ -205,6 +209,10 @@ export const resourceAccessSections: Record<
|
||||
title: 'Windows Desktop Access',
|
||||
component: WindowsDesktopAccessSection,
|
||||
},
|
||||
linux_desktop: {
|
||||
title: 'Linux Desktop Access',
|
||||
component: LinuxDesktopAccessSection,
|
||||
},
|
||||
git_server: {
|
||||
title: 'GitHub Organization Access',
|
||||
component: GitHubOrganizationAccessSection,
|
||||
@@ -931,6 +939,61 @@ export function WindowsDesktopAccessSection({
|
||||
);
|
||||
}
|
||||
|
||||
export function LinuxDesktopAccessSection({
|
||||
value,
|
||||
isProcessing,
|
||||
validation,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
visibleInputFields,
|
||||
}: SectionProps<
|
||||
LinuxDesktopAccess,
|
||||
LinuxDesktopAccessValidationResult,
|
||||
LinuxDesktopAccessInputFields
|
||||
>) {
|
||||
const show: LinuxDesktopAccessInputFields = visibleInputFields ?? {
|
||||
labels: true,
|
||||
logins: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{show.labels && (
|
||||
<Box mb={3}>
|
||||
<LabelsInput
|
||||
atLeastOneRow
|
||||
legend="Labels"
|
||||
disableBtns={isProcessing}
|
||||
labels={value.labels}
|
||||
setLabels={labels => onChange?.({ ...value, labels })}
|
||||
readOnly={readOnly}
|
||||
rule={readOnly ? undefined : precomputed(validation.fields.labels)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{show.logins && (
|
||||
<FieldSelectCreatable
|
||||
isMulti
|
||||
label="Logins"
|
||||
placeholder={readOnly ? '' : 'Type a login and press Enter'}
|
||||
toolTipContent="List of Linux logins allowed to use for desktop sessions."
|
||||
isDisabled={isProcessing}
|
||||
formatCreateLabel={label => `Login: ${label}`}
|
||||
components={{
|
||||
DropdownIndicator: null,
|
||||
}}
|
||||
openMenuOnClick={false}
|
||||
value={value.logins}
|
||||
onChange={logins => onChange?.({ ...value, logins: [...logins] })}
|
||||
menuPosition="fixed"
|
||||
readOnly={readOnly}
|
||||
rule={readOnly ? undefined : precomputed(validation.fields.logins)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitHubOrganizationAccessSection({
|
||||
value,
|
||||
isProcessing,
|
||||
|
||||
@@ -86,6 +86,7 @@ test('adding and removing sections', async () => {
|
||||
'Application Access',
|
||||
'Database Access',
|
||||
'Windows Desktop Access',
|
||||
'Linux Desktop Access',
|
||||
'GitHub Organization Access',
|
||||
]);
|
||||
|
||||
@@ -100,6 +101,7 @@ test('adding and removing sections', async () => {
|
||||
'Application Access',
|
||||
'Database Access',
|
||||
'Windows Desktop Access',
|
||||
'Linux Desktop Access',
|
||||
'GitHub Organization Access',
|
||||
]);
|
||||
|
||||
|
||||
@@ -303,6 +303,34 @@ describe.each<{ name: string; role: Role; model: RoleEditorModel }>([
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Linux desktop access',
|
||||
role: {
|
||||
...minimalRole(),
|
||||
spec: {
|
||||
...minimalRole().spec,
|
||||
allow: {
|
||||
linux_desktop_labels: { os: 'ubuntu' },
|
||||
linux_desktop_logins: ['alice', 'bob'],
|
||||
},
|
||||
},
|
||||
},
|
||||
model: {
|
||||
...minimalRoleModel(),
|
||||
resources: [
|
||||
{
|
||||
kind: 'linux_desktop',
|
||||
labels: [{ name: 'os', value: 'ubuntu' }],
|
||||
logins: [
|
||||
{ label: 'alice', value: 'alice' },
|
||||
{ label: 'bob', value: 'bob' },
|
||||
],
|
||||
hideValidationErrors: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'GitHub organizations',
|
||||
role: {
|
||||
|
||||
@@ -140,6 +140,7 @@ export type ResourceAccess =
|
||||
| AppAccess
|
||||
| DatabaseAccess
|
||||
| WindowsDesktopAccess
|
||||
| LinuxDesktopAccess
|
||||
| GitHubOrganizationAccess;
|
||||
|
||||
/**
|
||||
@@ -165,6 +166,7 @@ export type ResourceAccessKind =
|
||||
| 'app'
|
||||
| 'db'
|
||||
| 'windows_desktop'
|
||||
| 'linux_desktop'
|
||||
| 'git_server';
|
||||
|
||||
/*
|
||||
@@ -463,6 +465,20 @@ export type WindowsDesktopAccessInputFields =
|
||||
export type WindowsDesktopAccess = ResourceAccessBase<'windows_desktop'> &
|
||||
WindowsDesktopAccessFields;
|
||||
|
||||
/*
|
||||
* Models for the linux desktop resource access section.
|
||||
*/
|
||||
type LinuxDesktopAccessFields = {
|
||||
labels: UILabel[];
|
||||
logins: Option[];
|
||||
};
|
||||
|
||||
export type LinuxDesktopAccessInputFields =
|
||||
FieldTypesToBoolean<LinuxDesktopAccessFields>;
|
||||
|
||||
export type LinuxDesktopAccess = ResourceAccessBase<'linux_desktop'> &
|
||||
LinuxDesktopAccessFields;
|
||||
|
||||
/*
|
||||
* Models for the git server resource access section.
|
||||
*/
|
||||
@@ -651,6 +667,11 @@ export function newResourceAccess(
|
||||
roleVersion: RoleVersion
|
||||
): WindowsDesktopAccess;
|
||||
|
||||
export function newResourceAccess(
|
||||
kind: 'linux_desktop',
|
||||
roleVersion: RoleVersion
|
||||
): LinuxDesktopAccess;
|
||||
|
||||
export function newResourceAccess(
|
||||
kind: 'git_server',
|
||||
roleVersion: RoleVersion
|
||||
@@ -710,6 +731,13 @@ export function newResourceAccess(
|
||||
logins: [stringToOption('{{internal.windows_logins}}')],
|
||||
hideValidationErrors: true,
|
||||
};
|
||||
case 'linux_desktop':
|
||||
return {
|
||||
kind: 'linux_desktop',
|
||||
labels: [],
|
||||
logins: [stringToOption('{{internal.logins}}')],
|
||||
hideValidationErrors: true,
|
||||
};
|
||||
case 'git_server':
|
||||
return {
|
||||
kind: 'git_server',
|
||||
@@ -865,6 +893,10 @@ function roleConditionsToModel(
|
||||
windows_desktop_labels,
|
||||
windows_desktop_logins,
|
||||
|
||||
// Linux desktop access
|
||||
linux_desktop_labels,
|
||||
linux_desktop_logins,
|
||||
|
||||
// GitHub organization access
|
||||
github_permissions,
|
||||
|
||||
@@ -990,6 +1022,17 @@ function roleConditionsToModel(
|
||||
});
|
||||
}
|
||||
|
||||
const linuxDesktopLabelsModel = labelsToModel(linux_desktop_labels);
|
||||
const linuxDesktopLoginsModel = stringsToOptions(linux_desktop_logins ?? []);
|
||||
if (someNonEmpty(linuxDesktopLabelsModel, linuxDesktopLoginsModel)) {
|
||||
resources.push({
|
||||
kind: 'linux_desktop',
|
||||
labels: linuxDesktopLabelsModel,
|
||||
logins: linuxDesktopLoginsModel,
|
||||
hideValidationErrors: false,
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
model: gitHubOrganizationsModel,
|
||||
conversionErrors: gitHubOrganizationConversionErrors,
|
||||
@@ -1627,6 +1670,11 @@ export function roleEditorModelToRole(roleModel: RoleEditorModel): Role {
|
||||
role.spec.allow.windows_desktop_logins = optionsToStrings(res.logins);
|
||||
break;
|
||||
|
||||
case 'linux_desktop':
|
||||
role.spec.allow.linux_desktop_labels = labelsModelToLabels(res.labels);
|
||||
role.spec.allow.linux_desktop_logins = optionsToStrings(res.logins);
|
||||
break;
|
||||
|
||||
case 'git_server':
|
||||
const orgs = optionsToStrings(res.organizations);
|
||||
if (orgs.length > 0) {
|
||||
|
||||
@@ -111,6 +111,12 @@ describe('validateRoleEditorModel', () => {
|
||||
logins: [],
|
||||
hideValidationErrors: false,
|
||||
},
|
||||
{
|
||||
kind: 'linux_desktop',
|
||||
labels: [{ name: 'foo', value: 'bar' }],
|
||||
logins: [],
|
||||
hideValidationErrors: false,
|
||||
},
|
||||
];
|
||||
model.rules = [
|
||||
{
|
||||
@@ -146,7 +152,14 @@ describe('validateRoleEditorModel', () => {
|
||||
];
|
||||
const result = validateRoleEditorModel(model, undefined, undefined);
|
||||
expect(result.metadata.valid).toBe(true);
|
||||
expect(validity(result.resources)).toEqual([true, true, true, true, true]);
|
||||
expect(validity(result.resources)).toEqual([
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
]);
|
||||
expect(validity(result.rules)).toEqual([true, true]);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
@@ -215,6 +228,12 @@ describe('validateRoleEditorModel', () => {
|
||||
logins: [],
|
||||
hideValidationErrors: false,
|
||||
},
|
||||
{
|
||||
kind: 'linux_desktop',
|
||||
labels: [],
|
||||
logins: [],
|
||||
hideValidationErrors: false,
|
||||
},
|
||||
];
|
||||
const result = validateRoleEditorModel(model, undefined, undefined);
|
||||
expect(validity(result.resources)).toEqual([
|
||||
@@ -224,6 +243,7 @@ describe('validateRoleEditorModel', () => {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
ServerAccess,
|
||||
supportsKubernetesCustomResources,
|
||||
VerbModel,
|
||||
LinuxDesktopAccess,
|
||||
WindowsDesktopAccess,
|
||||
} from './standardmodel';
|
||||
|
||||
@@ -175,6 +176,8 @@ export function validateResourceAccess(
|
||||
return validateDatabaseAccess(resource);
|
||||
case 'windows_desktop':
|
||||
return validateWindowsDesktopAccess(resource);
|
||||
case 'linux_desktop':
|
||||
return validateLinuxDesktopAccess(resource);
|
||||
case 'git_server':
|
||||
return runRules(resource, gitHubOrganizationAccessValidationRules);
|
||||
default:
|
||||
@@ -188,6 +191,7 @@ export type ResourceAccessValidationResult =
|
||||
| AppAccessValidationResult
|
||||
| DatabaseAccessValidationResult
|
||||
| WindowsDesktopAccessValidationResult
|
||||
| LinuxDesktopAccessValidationResult
|
||||
| GitHubOrganizationAccessValidationResult;
|
||||
|
||||
const validKubernetesResource = (res: KubernetesResourceModel) => () => {
|
||||
@@ -551,6 +555,27 @@ export type WindowsDesktopAccessValidationResult = RuleSetValidationResult<
|
||||
typeof windowsDesktopAccessValidationRules
|
||||
>;
|
||||
|
||||
const validateLinuxDesktopAccess = (
|
||||
a: LinuxDesktopAccess
|
||||
): LinuxDesktopAccessValidationResult => {
|
||||
const result = runRules(a, linuxDesktopAccessValidationRules);
|
||||
if (a.labels.length === 0 && a.logins.length === 0) {
|
||||
result.valid = false;
|
||||
result.message = 'At least one label or login required';
|
||||
result.fields.labels.valid = false;
|
||||
result.fields.logins.valid = false;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const linuxDesktopAccessValidationRules = {
|
||||
labels: nonEmptyLabels,
|
||||
logins: alwaysValid,
|
||||
};
|
||||
export type LinuxDesktopAccessValidationResult = RuleSetValidationResult<
|
||||
typeof linuxDesktopAccessValidationRules
|
||||
>;
|
||||
|
||||
const gitHubOrganizationAccessValidationRules = {
|
||||
organizations: requiredField<Option>('At least one organization required'),
|
||||
};
|
||||
|
||||
@@ -226,7 +226,7 @@ export const SupportSectionCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
const IconBox = styled('span', {
|
||||
export const IconBox = styled('span', {
|
||||
base: {
|
||||
lineHeight: 0,
|
||||
padding: 2,
|
||||
|
||||
@@ -171,6 +171,15 @@ export type WindowsDesktopResourceAccess = {
|
||||
windows_desktop_logins?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields related to linux desktop access.
|
||||
*/
|
||||
export type LinuxDesktopResourceAccess = {
|
||||
linux_desktop_labels?: Labels;
|
||||
|
||||
linux_desktop_logins?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A set of conditions that must be matched to allow or deny access. Fields
|
||||
* follow the snake case convention to match the wire format.
|
||||
@@ -182,7 +191,8 @@ export type RoleConditions = {
|
||||
GitHubResourceAccess &
|
||||
KubernetesResourceAccess &
|
||||
ServerResourceAccess &
|
||||
WindowsDesktopResourceAccess;
|
||||
WindowsDesktopResourceAccess &
|
||||
LinuxDesktopResourceAccess;
|
||||
|
||||
export type Labels = Record<string, string | string[]>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user