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

test: rbac config [TESTENG-108] #10032

Merged
merged 5 commits into from
Oct 28, 2024
Merged
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
47 changes: 47 additions & 0 deletions .circleci/devcluster/react-rbac.devcluster.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
temp_dir: /tmp/priority_scheduler

stages:
- db:
name: db

- master:
pre:
- sh: make -C tools prep-root
config_file:
security:
initial_user_password: $INITIAL_USER_PASSWORD
authz:
rbac_ui_enabled: true
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👁️ 👁️

port: 8082
db:
host: localhost
port: 5432
password: postgres
user: postgres
name: determined
checkpoint_storage:
type: shared_fs
host_path: /tmp
storage_path: determined-cp
log:
level: debug
root: tools/build
cache:
cache_dir: /tmp/determined-cache
launch_error: false
telemetry:
enabled: false
resource_manager:
default_aux_resource_pool: default
default_compute_resource_pool: default
type: agent

- agent:
name: agent1
config_file:
master_host: 127.0.0.1
master_port: 8082
agent_id: agent1
container_master_host: $DOCKER_LOCALHOST
agent_reconnect_attempts: 24
agent_reconnect_backoff: 5
24 changes: 23 additions & 1 deletion .circleci/real_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,12 @@ jobs:

test-e2e-react:
parameters:
devcluster-config:
type: enum
enum:
- react.devcluster.yaml
- react-rbac.devcluster.yaml
- react-sso.devcluster.yaml
ee:
type: boolean
default: false
Expand Down Expand Up @@ -2134,7 +2140,7 @@ jobs:
- run: make -C agent get-deps
- install-devcluster
- start-devcluster:
devcluster-config: react.devcluster.yaml
devcluster-config: <<parameters.devcluster-config>>
- run: make -C webui/react get-playwright-ci
- run: SERVER_ADDRESS=${PW_SERVER_ADDRESS} npm run build --prefix webui/react
- wait-for-master:
Expand Down Expand Up @@ -4097,6 +4103,7 @@ workflows:
dev-mode: true
- test-e2e-react:
name: test-e2e-react-oss
devcluster-config: react.devcluster.yaml
requires:
- build-go-oss
context:
Expand Down Expand Up @@ -4186,13 +4193,27 @@ workflows:
- test-e2e-react:
name: test-e2e-react-ee
ee: true
devcluster-config: react-rbac.devcluster.yaml
requires:
- build-go-ee
context:
- playwright
- github-read
- dev-ci-cluster-default-user-credentials
filters: *any-upstream
# this will be used once sso tests are prioritized (after rbac tests)
# - test-e2e-react:
# name: test-e2e-react-ee-sso
# ee: true
# devcluster-config: react-sso.devcluster.yaml
# playwright-options: "-g sso"
# requires:
# - build-go-ee
# context:
# - playwright
# - github-read
# - dev-ci-cluster-default-user-credentials
# filters: *any-upstream
- build-docs:
requires:
- build-helm
Expand Down Expand Up @@ -5428,6 +5449,7 @@ workflows:
context: github-read
- test-e2e-react:
ee: << pipeline.parameters.ee >>
devcluster-config: react.devcluster.yaml
playwright-options: << pipeline.parameters.e2e-react >>
requires:
- build-go
Expand Down
51 changes: 51 additions & 0 deletions webui/react/src/e2e/fixtures/api.roles.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import streamConsumers from 'stream/consumers';

import _ from 'lodash';

import { RBACApi, V1AssignRolesRequest, V1AssignRolesResponse } from 'services/api-ts-sdk/api';

import { ApiAuthFixture } from './api.auth.fixture';

export class ApiRoleFixture {
readonly apiAuth: ApiAuthFixture;
constructor(apiAuth: ApiAuthFixture) {
this.apiAuth = apiAuth;
}

new({ roleProps = {} } = {}): V1AssignRolesRequest {
const defaults = {};
return {
...defaults,
...roleProps,
};
}

private static normalizeUrl(url: string): string {
if (url.endsWith('/')) {
return url.substring(0, url.length - 1);
}
return url;
}

private async startRoleRequest(): Promise<RBACApi> {
return new RBACApi(
{ apiKey: await this.apiAuth.getBearerToken() },
ApiRoleFixture.normalizeUrl(this.apiAuth.baseURL),
fetch,
);
}

async createAssignment(req: V1AssignRolesRequest): Promise<V1AssignRolesResponse> {
const roleResp = await (await this.startRoleRequest())
.assignRoles(req, {})
.catch(async function (error) {
const respBody = await streamConsumers.text(error.body);
throw new Error(
`Create Assignment Failed: ${error.status} Request: ${JSON.stringify(
req,
)} Response: ${respBody}`,
);
});
return _.merge(req, roleResp);
}
}
7 changes: 5 additions & 2 deletions webui/react/src/e2e/fixtures/auth.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Page } from '@playwright/test';

import { expect } from 'e2e/fixtures/global-fixtures';
import { DefaultRoute } from 'e2e/models/pages/DefaultRoute';
import { SignIn } from 'e2e/models/pages/SignIn';
import { password, username } from 'e2e/utils/envVars';

Expand All @@ -18,7 +19,7 @@ export class AuthFixture {
}

async login({
expectedURL = /dashboard/,
expectedURL,
username = this.#USERNAME,
password = this.#PASSWORD,
}: {
Expand All @@ -35,7 +36,9 @@ export class AuthFixture {
await detAuth.username.pwLocator.fill(username);
await detAuth.password.pwLocator.fill(password);
await detAuth.submit.pwLocator.click();
await this.#page.waitForURL(expectedURL);

const defaultPage = new DefaultRoute(this.#page);
await this.#page.waitForURL(expectedURL ?? defaultPage.url);
}

async logout(): Promise<void> {
Expand Down
16 changes: 15 additions & 1 deletion webui/react/src/e2e/fixtures/global-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {

import { ApiAuthFixture } from './api.auth.fixture';
import { ApiProjectFixture } from './api.project.fixture';
import { ApiRoleFixture } from './api.roles.fixture';
import { ApiUserFixture } from './api.user.fixture';
import { ApiWorkspaceFixture } from './api.workspace.fixture';
import { AuthFixture } from './auth.fixture';
Expand All @@ -39,6 +40,7 @@ type CustomWorkerFixtures = {
newProject: { request: V1PostProjectRequest; response: V1PostProjectResponse };
backgroundApiAuth: ApiAuthFixture;
backgroundApiUser: ApiUserFixture;
backgroundApiRole: ApiRoleFixture;
backgroundApiWorkspace: ApiWorkspaceFixture;
backgroundApiProject: ApiProjectFixture;
backgroundAuthedPage: Page;
Expand Down Expand Up @@ -128,6 +130,13 @@ export const test = baseTest.extend<CustomFixtures, CustomWorkerFixtures>({
},
{ scope: 'worker' },
],
backgroundApiRole: [
async ({ backgroundApiAuth }, use) => {
const backgroundApiRole = new ApiRoleFixture(backgroundApiAuth);
await use(backgroundApiRole);
},
{ scope: 'worker' },
],
/**
* Allows calling the user api without a page so that it can run in beforeAll(). You will need to get a bearer
* token by calling backgroundApiUser.apiAuth.loginAPI(). This will also provision a page in the background which
Expand Down Expand Up @@ -181,7 +190,7 @@ export const test = baseTest.extend<CustomFixtures, CustomWorkerFixtures>({
* Creates an admin and logs in as that admin for the duraction of the test suite
*/
newAdmin: [
async ({ backgroundApiUser }, use, workerInfo) => {
async ({ backgroundApiUser, backgroundApiRole }, use, workerInfo) => {
const request = backgroundApiUser.new({
userProps: {
user: {
Expand All @@ -192,6 +201,11 @@ export const test = baseTest.extend<CustomFixtures, CustomWorkerFixtures>({
},
});
const adminUser = await backgroundApiUser.createUser(request);
await backgroundApiRole.createAssignment({
userRoleAssignments: [
{ roleAssignment: { role: { roleId: 1 } }, userId: adminUser.user!.id! },
],
});
await use({ request, response: adminUser });
await backgroundApiUser.patchUser(adminUser.user!.id!, { active: false });
},
Expand Down
27 changes: 16 additions & 11 deletions webui/react/src/e2e/fixtures/user.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { expect } from 'e2e/fixtures/global-fixtures';
import { UserManagement } from 'e2e/models/pages/Admin/UserManagement';
import { safeName } from 'e2e/utils/naming';
import { repeatWithFallback } from 'e2e/utils/polling';
import { isRbacEnabled } from 'e2e/utils/rbac';
import { TestUser } from 'e2e/utils/users';

interface CreateUserFields {
Expand Down Expand Up @@ -43,16 +44,18 @@ export class UserFixture {
);
}

const checkedAttribute =
await this.userManagementPage.createUserModal.adminToggle.pwLocator.getAttribute(
'aria-checked',
);
if (checkedAttribute === null) {
throw new Error('Expected attribute aria-checked to be present.');
}
const adminState = JSON.parse(checkedAttribute);
if (!!formValues.admin !== adminState) {
await this.userManagementPage.createUserModal.adminToggle.pwLocator.click();
if (!isRbacEnabled()) {
const checkedAttribute =
await this.userManagementPage.createUserModal.adminToggle.pwLocator.getAttribute(
'aria-checked',
);
if (checkedAttribute === null) {
throw new Error('Expected attribute aria-checked to be present.');
}
const adminState = JSON.parse(checkedAttribute);
if (!!formValues.admin !== adminState) {
await this.userManagementPage.createUserModal.adminToggle.pwLocator.click();
}
}

// password and username are required to create a user; if these are filled, submit should be enabled
Expand Down Expand Up @@ -180,7 +183,9 @@ export class UserFixture {
} else {
await row.user.alias.pwLocator.waitFor({ state: 'hidden' });
}
await expect(row.role.pwLocator).toContainText(user.admin ? 'Admin' : 'Member');
if (!isRbacEnabled()) {
await expect(row.role.pwLocator).toContainText(user.admin ? 'Admin' : 'Member');
}
await expect(row.status.pwLocator).toContainText(user.active ? 'Active' : 'Inactive');
}

Expand Down
11 changes: 11 additions & 0 deletions webui/react/src/e2e/models/pages/DefaultRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DeterminedPage } from 'e2e/models/common/base/BasePage';
import { isRbacEnabled } from 'e2e/utils/rbac';

/**
* Represents the DefaultRoute page from src/pages/DefaultRoute.tsx
*/
export class DefaultRoute extends DeterminedPage {
// only redirects to Dashboard or WorkspaceList page
readonly title = isRbacEnabled() ? 'Workspaces' : 'Home';
readonly url = isRbacEnabled() ? /workspaces/ : /dashboard/;
}
6 changes: 4 additions & 2 deletions webui/react/src/e2e/tests/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from 'e2e/fixtures/global-fixtures';
import { Cluster } from 'e2e/models/pages/Cluster';
import { DefaultRoute } from 'e2e/models/pages/DefaultRoute';
import { SignIn } from 'e2e/models/pages/SignIn';

test.describe('Authentication', () => {
Expand All @@ -13,8 +14,9 @@ test.describe('Authentication', () => {
test('Login and Logout', async ({ page, auth }) => {
await test.step('Login', async () => {
await auth.login();
await expect(page).toHaveDeterminedTitle('Home');
await expect(page).toHaveURL(/dashboard/);
const defaultPage = new DefaultRoute(page);
await expect(page).toHaveDeterminedTitle(defaultPage.title);
await expect(page).toHaveURL(defaultPage.url);
});

await test.step('Logout', async () => {
Expand Down
5 changes: 2 additions & 3 deletions webui/react/src/e2e/tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ test.describe('Navigation', () => {
// we need any page to access the sidebar, and i haven't modeled the homepage yet
const userManagementPage = new UserManagement(authedPage);

await test.step('Login steps', async () => {
await expect(authedPage).toHaveDeterminedTitle('Home');
await expect(authedPage).toHaveURL(/dashboard/);
await test.step('Load page', async () => {
await userManagementPage.goto();
});

await test.step('Uncategorized', async () => {
Expand Down
18 changes: 18 additions & 0 deletions webui/react/src/e2e/utils/rbac.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { detExecSync } from 'e2e/utils/detCLI';

let rbacEnabled: boolean;

const getRbacEnabled = (): boolean => {
const masterInfo = detExecSync('master info');
const regexp = /rbacEnabled:\s*(?<enabled>true|false)/;

const { groups } = regexp.exec(masterInfo) || {};

return groups?.enabled === 'true';
};

export const isRbacEnabled = (): boolean => {
if (rbacEnabled === undefined) rbacEnabled = getRbacEnabled();

return rbacEnabled;
};
Loading