Skip to content

Commit

Permalink
Make sure we don't break the role editor for some preset roles
Browse files Browse the repository at this point in the history
Adds a test that verifies that the built-in roles, as pulled from our Go
source codes, can be unambiguously translated to the standard editor
model. Also adds one currently missing field (`github_organizations`).
  • Loading branch information
bl-nero committed Jan 29, 2025
1 parent cdcd860 commit 8bb5423
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 10 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1843,3 +1843,7 @@ create-github-release:
.PHONY: go-mod-tidy-all
go-mod-tidy-all:
find . -type "f" -name "go.mod" -execdir go mod tidy \;

.PHONY: dump-preset-roles
dump-preset-roles:
GOOS=$(OS) GOARCH=$(ARCH) $(CGOFLAG) go run ./build.assets/dump-preset-roles/main.go
54 changes: 54 additions & 0 deletions build.assets/dump-preset-roles/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

// A tool that dumps preset roles in a temporary JSON file that will later be
// used in TypeScript tests to make sure that the standard role editor can
// unambiguously represent a preset role.

package main

import (
"encoding/json"
"log"
"os"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/services"
)

func main() {
roles := auth.GetPresetRoles()
rolesByName := make(map[string]types.Role)
for _, role := range roles {
services.CheckAndSetDefaults(role)
rolesByName[role.GetName()] = role
}

rolesJSON, err := json.Marshal(rolesByName)
if err != nil {
log.Fatalf("Could not marshal preset roles as JSON: %s", err)
}

err = os.MkdirAll("./tmp", 0755)
if err != nil {
log.Fatalf("Could not create the ./tmp directory: %s", err)
}

if err = os.WriteFile("./tmp/preset-roles.json", rolesJSON, 0744); err != nil {
log.Fatalf("Could not write JSON for preset roles: %s", err)
}
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
"storybook": "./web/scripts/run-storybook.sh",
"storybook-smoke-test": "storybook dev -p 9002 -c web/.storybook --ci --smoke-test",
"test-storybook": "NODE_TLS_REJECT_UNAUTHORIZED=0 test-storybook -c web/.storybook --url https://localhost:9002 --skipTags=skip-test --browsers=chromium",
"test": "jest",
"test-coverage": "jest --coverage && web/scripts/print-coverage-link.sh",
"test": "make dump-preset-roles && jest",
"test-coverage": "make dump-preset-roles && jest --coverage && web/scripts/print-coverage-link.sh",
"test-update-snapshot": "pnpm run test --updateSnapshot",
"tdd": "jest --watch",
"tdd": "make dump-preset-roles && jest --watch",
"lint": "pnpm eslint && pnpm prettier-check",
"lint-fix": "pnpm eslint --fix && pnpm prettier-write",
"eslint": "eslint --quiet '+(e|web)/**/*.{ts,tsx,js,jsx,mts}'",
Expand Down
1 change: 1 addition & 0 deletions web/packages/build/jest/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = {
'^e-teleport/(.*)$': '<rootDir>/e/web/teleport/src/$1',
'^gen-proto-js/(.*)$': '<rootDir>/gen/proto/js/$1',
'^gen-proto-ts/(.*)$': '<rootDir>/gen/proto/ts/$1',
'^tmp/(.*)$': '<rootDir>/tmp/$1',
},
// Keep pre-v29 snapshot format to avoid existing snapshots breaking.
// https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function RoleEditorAdapter({
}, [originalContent]);

const onRoleUpdate = useCallback(
debounce(roleDiffProps.updateRoleDiff, 500),
debounce(roleDiffProps?.updateRoleDiff, 500),
[]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { RoleVersion } from 'teleport/services/resources';
import {
AppAccessSection,
DatabaseAccessSection,
GitHubOrganizationAccessSection,
KubernetesAccessSection,
ServerAccessSection,
WindowsDesktopAccessSection,
Expand All @@ -35,13 +36,15 @@ import {
AppAccess,
DatabaseAccess,
defaultRoleVersion,
GitHubOrganizationAccess,
KubernetesAccess,
newResourceAccess,
ServerAccess,
WindowsDesktopAccess,
} from './standardmodel';
import { StatefulSection } from './StatefulSection';
import {
GitHubOrganizationAccessValidationResult,
ResourceAccessValidationResult,
validateResourceAccess,
} from './validation';
Expand Down Expand Up @@ -518,6 +521,46 @@ describe('WindowsDesktopAccessSection', () => {
});
});

describe('GitHubOrganizationAccessSection', () => {
const setup = () => {
const onChange = jest.fn();
let validator: Validator;
render(
<StatefulSection<
GitHubOrganizationAccess,
GitHubOrganizationAccessValidationResult
>
component={GitHubOrganizationAccessSection}
defaultValue={newResourceAccess('git_server', defaultRoleVersion)}
onChange={onChange}
validatorRef={v => {
validator = v;
}}
validate={validateResourceAccess}
/>
);
return { user: userEvent.setup(), onChange, validator };
};

test('editing', async () => {
const { onChange } = setup();
await selectEvent.create(
screen.getByLabelText('Organization Names'),
'illuminati',
{
createOptionText: 'Organization: illuminati',
}
);
expect(onChange).toHaveBeenLastCalledWith({
kind: 'git_server',
organizations: [
expect.objectContaining({ value: '{{internal.github_orgs}}' }),
expect.objectContaining({ label: 'illuminati', value: 'illuminati' }),
],
} as GitHubOrganizationAccess);
});
});

const reactSelectValueContainer = (input: HTMLInputElement) =>
// eslint-disable-next-line testing-library/no-node-access
input.closest('.react-select__value-container');
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { SectionBox, SectionProps, SectionPropsWithDispatch } from './sections';
import {
AppAccess,
DatabaseAccess,
GitHubOrganizationAccess,
KubernetesAccess,
kubernetesResourceKindOptions,
KubernetesResourceModel,
Expand All @@ -54,6 +55,7 @@ import {
import {
AppAccessValidationResult,
DatabaseAccessValidationResult,
GitHubOrganizationAccessValidationResult,
KubernetesAccessValidationResult,
KubernetesResourceValidationResult,
ResourceAccessValidationResult,
Expand Down Expand Up @@ -140,6 +142,7 @@ const allResourceAccessKinds: ResourceAccessKind[] = [
'app',
'db',
'windows_desktop',
'git_server',
];

/** Maps resource access kind to UI component configuration. */
Expand Down Expand Up @@ -176,6 +179,11 @@ export const resourceAccessSections: Record<
tooltip: 'Configures access to Windows desktops',
component: WindowsDesktopAccessSection,
},
git_server: {
title: 'GitHub Organizations',
tooltip: 'Configures access to GitHub organizations and their repositories',
component: GitHubOrganizationAccessSection,
},
};

/**
Expand Down Expand Up @@ -595,6 +603,32 @@ export function WindowsDesktopAccessSection({
);
}

export function GitHubOrganizationAccessSection({
value,
isProcessing,
onChange,
}: SectionProps<
GitHubOrganizationAccess,
GitHubOrganizationAccessValidationResult
>) {
return (
<FieldSelectCreatable
isMulti
label="Organization Names"
toolTipContent="A list of GitHub organization names that this role is allowed to use"
placeholder="Type an organization name and press Enter"
isDisabled={isProcessing}
formatCreateLabel={label => `Organization: ${label}`}
components={{
DropdownIndicator: null,
}}
openMenuOnClick={false}
value={value.organizations}
onChange={organizations => onChange?.({ ...value, organizations })}
/>
);
}

// TODO(bl-nero): This should ideally use tonal neutral 1 from the opposite
// theme as background.
const MarkInverse = styled(Mark)`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ test('adding and removing sections', async () => {
'Applications',
'Databases',
'Windows Desktops',
'GitHub Organizations',
]);

await user.click(screen.getByRole('menuitem', { name: 'Servers' }));
Expand All @@ -76,6 +77,7 @@ test('adding and removing sections', async () => {
'Applications',
'Databases',
'Windows Desktops',
'GitHub Organizations',
]);

await user.click(screen.getByRole('menuitem', { name: 'Kubernetes' }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import presetRoles from 'tmp/preset-roles.json';

import { Label as UILabel } from 'teleport/components/LabelsInput/LabelsInput';
import {
CreateDBUserMode,
CreateHostUserMode,
GitHubPermission,
KubernetesResource,
Labels,
RequireMFAType,
Expand Down Expand Up @@ -252,6 +255,31 @@ describe.each<{ name: string; role: Role; model: RoleEditorModel }>([
},
},

{
name: 'GitHub organization',
role: {
...minimalRole(),
spec: {
...minimalRole().spec,
allow: {
github_permissions: [{ orgs: ['illuminati', 'reptilians'] }],
},
},
},
model: {
...minimalRoleModel(),
resources: [
{
kind: 'git_server',
organizations: [
{ label: 'illuminati', value: 'illuminati' },
{ label: 'reptilians', value: 'reptilians' },
],
},
],
},
},

{
name: 'Options object',
role: {
Expand Down Expand Up @@ -564,6 +592,31 @@ describe('roleToRoleEditorModel', () => {
},
},

{
name: 'unknown field in github_permissions',
role: {
...minRole,
spec: {
...minRole.spec,
allow: {
...minRole.spec.allow,
github_permissions: [
{ orgs: ['foo'], unknownField: 123 } as GitHubPermission,
],
},
},
},
model: {
...roleModelWithReset,
resources: [
{
kind: 'git_server',
organizations: [{ label: 'foo', value: 'foo' }],
},
],
},
},

{
name: 'unknown fields in Rule',
role: {
Expand Down Expand Up @@ -992,6 +1045,40 @@ describe('roleToRoleEditorModel', () => {
],
} as RoleEditorModel);
});

test('multiple github_permissions', () => {
expect(
roleToRoleEditorModel({
...minimalRole(),
spec: {
...minimalRole().spec,
allow: {
...minimalRole().spec.allow,
github_permissions: [{ orgs: ['foo'] }, { orgs: ['bar'] }],
},
},
})
).toEqual({
...minimalRoleModel(),
resources: [
{
kind: 'git_server',
organizations: [
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
],
},
],
} as RoleEditorModel);
});

it.each(['access', 'editor', 'auditor'])(
'supports the preset "%s" role',
roleName => {
const { requiresReset } = roleToRoleEditorModel(presetRoles[roleName]);
expect(requiresReset).toBe(false);
}
);
});

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

0 comments on commit 8bb5423

Please sign in to comment.