test(e2e): Add Admin UI test for OIDC auth method (#2638)

* test(e2e): Add managed group check in LDAP test

* test(e2e): Add oidc test

* CR: Suppress warning

* CR: Use toHaveText
This commit is contained in:
Michael Li authored and GitHub committed 2025-01-03 14:19:33 -05:00
1 parent ed569306bb
commit ca4aad0a2b
5 files changed
+530 -11

No files matched your search

+91
View File
@@ -33,6 +33,97 @@ export class AuthMethodsPage extends BaseResourcePage {
return authMethodName;
}
/**
* Creates a new OIDC Auth Method. Assumes you have selected the desired scope.
* @param {string} issuer OIDC issuer
* @param {string} clientId OIDC client ID
* @param {string} clientSecret OIDC client secret
* @returns Name of the auth method
*/
async createOidcAuthMethod(issuer, clientId, clientSecret, boundaryAddr) {
const authMethodName = 'Auth Method ' + nanoid();
await this.page
.getByRole('navigation', { name: 'IAM' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await this.page.getByRole('button', { name: 'New' }).click();
await this.page.getByRole('link', { name: 'OIDC' }).click();
await this.page.getByLabel('Name').fill(authMethodName);
await this.page.getByLabel('Description').fill('OIDC Auth Method');
await this.page.getByLabel('Issuer').fill(issuer);
await this.page.getByLabel('Client ID').fill(clientId);
await this.page.getByLabel('Client Secret').fill(clientSecret);
await this.page
.getByRole('group', { name: 'Signing Algorithms' })
.getByRole('combobox')
.selectOption('RS256');
await this.page
.getByRole('group', { name: 'Signing Algorithms' })
.getByRole('button', { name: 'Add' })
.click();
await this.page
.getByRole('group', { name: 'Claims Scopes' })
.getByRole('textbox')
.last()
.fill('groups');
await this.page
.getByRole('group', { name: 'Claims Scopes' })
.getByRole('button', { name: 'Add' })
.click();
await this.page
.getByRole('group', { name: 'Claims Scopes' })
.getByRole('textbox')
.last()
.fill('user');
await this.page
.getByRole('group', { name: 'Claims Scopes' })
.getByRole('button', { name: 'Add' })
.click();
await this.page
.getByRole('group', { name: 'Account Claim Maps' })
.getByLabel('From Claim')
.last()
.fill('username');
await this.page
.getByRole('group', { name: 'Account Claim Maps' })
.getByLabel('To Claim')
.last()
.selectOption('name');
await this.page
.getByRole('group', { name: 'Account Claim Maps' })
.getByRole('button', { name: 'Add' })
.click();
await this.page
.getByRole('group', { name: 'Account Claim Maps' })
.getByLabel('From Claim')
.last()
.fill('email');
await this.page
.getByRole('group', { name: 'Account Claim Maps' })
.getByLabel('To Claim')
.last()
.selectOption('email');
await this.page
.getByRole('group', { name: 'Account Claim Maps' })
.getByRole('button', { name: 'Add' })
.click();
await this.page.getByLabel('Maximum Age').fill('20');
await this.page.getByLabel('API URL Prefix').fill(boundaryAddr);
await this.page.getByRole('button', { name: 'Save' }).click();
await this.dismissSuccessAlert();
await expect(
this.page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText(authMethodName),
).toBeVisible();
return authMethodName;
}
/**
* Makes the first available auth method primary.
* Assumes you have created new auth method.
+56 -11
View File
@@ -106,8 +106,8 @@ test('Set up LDAP auth method @ce @ent @docker', async ({
).toBeVisible();
// Change state to active-public
page.getByTitle('Inactive').click();
page.getByText('Public').click();
await page.getByTitle('Inactive').click();
await page.getByText('Public').click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
@@ -190,8 +190,13 @@ test('Set up LDAP auth method @ce @ent @docker', async ({
.click();
await page.getByRole('link', { name: ldapAuthMethodName }).click();
await page.getByRole('link', { name: 'Accounts' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText('Accounts'),
).toBeVisible();
const headersCount = await page
let headersCount = await page
.getByRole('table')
.getByRole('columnheader')
.count();
@@ -211,21 +216,61 @@ test('Set up LDAP auth method @ce @ent @docker', async ({
}
expect(
await page
page
.getByRole('cell', { name: ldapAccountName })
.locator('..')
.getByRole('cell')
.nth(fullNameIndex)
.innerText(),
).toBe(ldapUserName);
.nth(fullNameIndex),
).toHaveText(ldapUserName);
expect(
await page
page
.getByRole('cell', { name: ldapAccountName })
.locator('..')
.getByRole('cell')
.nth(emailIndex)
.innerText(),
).toBe(ldapUserName + '@mail.com');
.nth(emailIndex),
).toHaveText(ldapUserName + '@mail.com');
// View the Managed Group
await page.getByRole('link', { name: 'Managed Groups' }).click();
await page.getByRole('link', { name: ldapManagedGroupName }).click();
await page.getByRole('link', { name: 'Members' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText('Members'),
).toBeVisible();
headersCount = await page
.getByRole('table')
.getByRole('columnheader')
.count();
for (let i = 0; i < headersCount; i++) {
const header = await page
.getByRole('table')
.getByRole('columnheader')
.nth(i)
.innerText();
if (header == 'Full Name') {
fullNameIndex = i;
} else if (header == 'Email') {
emailIndex = i;
}
}
expect(
page
.getByRole('cell', { name: ldapAccountName })
.locator('..')
.getByRole('cell')
.nth(fullNameIndex),
).toHaveText(ldapUserName);
expect(
page
.getByRole('cell', { name: ldapAccountName })
.locator('..')
.getByRole('cell')
.nth(emailIndex),
).toHaveText(ldapUserName + '@mail.com');
// View the User account and verify attributes
await page
@@ -0,0 +1,264 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { test } from '../../global-setup.js';
import { expect } from '@playwright/test';
import { execSync } from 'child_process';
import * as boundaryCli from '../../helpers/boundary-cli';
import * as vaultCli from '../../helpers/vault-cli';
import { AuthMethodsPage } from '../pages/auth-methods.js';
import { LoginPage } from '../pages/login.js';
import { OrgsPage } from '../pages/orgs.js';
import { RolesPage } from '../pages/roles.js';
// Reset storage state for this file to avoid being authenticated
test.use({ storageState: { cookies: [], origins: [] } });
test.beforeAll(async () => {
await boundaryCli.checkBoundaryCli();
await vaultCli.checkVaultCli();
});
test.beforeEach(async () => {
execSync(`vault auth disable userpass`);
});
test('Set up OIDC auth method @ce @ent @docker @aws', async ({
page,
context,
baseURL,
adminAuthMethodId,
adminLoginName,
adminPassword,
vaultAddr,
}) => {
await page.goto('/');
let orgName;
let policyName;
try {
const userName = 'end-user';
const password = 'password123';
const email = 'vault@hashicorp.com';
const { issuer, clientId, clientSecret, authPolicyName } =
await vaultCli.setupVaultOidc(
vaultAddr,
userName,
password,
email,
baseURL,
);
policyName = authPolicyName;
// Log in
const loginPage = new LoginPage(page);
await loginPage.login(adminLoginName, adminPassword);
await expect(
page.getByRole('navigation', { name: 'breadcrumbs' }).getByText('Orgs'),
).toBeVisible();
// Create OIDC Auth Method
const orgsPage = new OrgsPage(page);
orgName = await orgsPage.createOrg();
const authMethodsPage = new AuthMethodsPage(page);
const oidcAuthMethodName = await authMethodsPage.createOidcAuthMethod(
issuer,
clientId,
clientSecret,
baseURL,
);
// Change OIDC Auth Method state to active-public
await page.getByTitle('Inactive').click();
await page.getByText('Public').click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
// Set auth method as primary
await page.getByText('Manage', { exact: true }).click();
await page.getByRole('button', { name: 'Make Primary' }).click();
await page.getByRole('button', { name: 'OK' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
// Create an OIDC managed group
await page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText(oidcAuthMethodName)
.click();
await page.getByText('Manage', { exact: true }).click();
await page.getByRole('link', { name: 'Create Managed Group' }).click();
const oidcManagedGroupName = 'OIDC Managed Group';
await page.getByLabel('Name (Optional)').fill(oidcManagedGroupName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByLabel('Filter').fill(`"engineering" in "/userinfo/groups"`);
await page.getByRole('button', { name: 'Save' }).click();
await expect(
page.getByRole('alert').getByText('Success', { exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
// Create a role and add LDAP managed group to role
const rolesPage = new RolesPage(page);
await rolesPage.createRole();
await rolesPage.addPrincipalToRole(oidcManagedGroupName);
// Log in using oidc account
await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click();
await page.getByRole('link', { name: orgName }).click();
await page.getByRole('link', { name: oidcAuthMethodName }).click();
const pagePromise = context.waitForEvent('page');
await page.getByRole('button', { name: 'Sign In' }).click();
const vaultPage = await pagePromise;
await vaultPage.getByLabel('Method').selectOption('Username');
await vaultPage.getByLabel('Username').fill(userName);
await vaultPage.getByLabel('Password').fill(password);
await vaultPage.getByRole('button', { name: 'Sign In' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText('Projects'),
).toBeVisible();
// Log back in as an admin
// WORKAROUND: Currently, users logging using OIDC don't have a username
// displayed in the UI, so there's no simple locator to access this menu.
await page.locator('details').filter({ hasText: 'Sign Out' }).click();
await page.getByRole('button', { name: 'Sign Out' }).click();
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
await loginPage.login(adminLoginName, adminPassword);
await expect(
page.getByRole('navigation', { name: 'breadcrumbs' }).getByText('Orgs'),
).toBeVisible();
// View the OIDC account and verify account attributes
await page.getByRole('link', { name: orgName }).click();
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await page.getByRole('link', { name: oidcAuthMethodName }).click();
await page.getByRole('link', { name: 'Accounts' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText('Accounts'),
).toBeVisible();
let headersCount = await page
.getByRole('table')
.getByRole('columnheader')
.count();
let fullNameIndex;
let emailIndex;
for (let i = 0; i < headersCount; i++) {
const header = await page
.getByRole('table')
.getByRole('columnheader')
.nth(i)
.innerText();
if (header == 'Full Name') {
fullNameIndex = i;
} else if (header == 'Email') {
emailIndex = i;
}
}
expect(
page
.getByRole('cell', { name: userName })
.locator('..')
.getByRole('cell')
.nth(fullNameIndex),
).toHaveText(userName);
expect(
page
.getByRole('cell', { name: userName })
.locator('..')
.getByRole('cell')
.nth(emailIndex),
).toHaveText(email);
// View the OIDC Managed Group and verify member in managed group
await page.getByRole('link', { name: 'Managed Groups' }).click();
await page.getByRole('link', { name: oidcManagedGroupName }).click();
await page.getByRole('link', { name: 'Members' }).click();
await expect(
page
.getByRole('navigation', { name: 'breadcrumbs' })
.getByText('Members'),
).toBeVisible();
headersCount = await page
.getByRole('table')
.getByRole('columnheader')
.count();
for (let i = 0; i < headersCount; i++) {
const header = await page
.getByRole('table')
.getByRole('columnheader')
.nth(i)
.innerText();
if (header == 'Full Name') {
fullNameIndex = i;
} else if (header == 'Email') {
emailIndex = i;
}
}
expect(
page
.getByRole('cell', { name: userName })
.locator('..')
.getByRole('cell')
.nth(fullNameIndex),
).toHaveText(userName);
expect(
page
.getByRole('cell', { name: userName })
.locator('..')
.getByRole('cell')
.nth(emailIndex),
).toHaveText(email);
// View the User account and verify attributes
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('link', { name: 'Users' })
.click();
await page.getByRole('cell', { hasText: email }).getByRole('link').click();
await page.getByRole('link', { name: 'Accounts' }).click();
expect(
await page
.getByRole('table')
.getByRole('row')
.nth(1) // Account row
.getByRole('cell')
.nth(0) // Name field
.innerText(),
).toContain(email);
} finally {
execSync(`vault auth disable userpass`);
execSync(`vault policy delete ${policyName}`);
if (orgName) {
await boundaryCli.authenticateBoundary(
baseURL,
adminAuthMethodId,
adminLoginName,
adminPassword,
);
const orgId = await boundaryCli.getOrgIdFromName(orgName);
if (orgId) {
await boundaryCli.deleteScope(orgId);
}
}
}
});
+6
View File
@@ -0,0 +1,6 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
path "identity/oidc/provider/my-provider/authorize" {
capabilities = [ "read" ]
}
+113
View File
@@ -50,3 +50,116 @@ export async function getVaultToken(boundaryPolicyName, secretPolicyName) {
return clientToken;
}
/**
* Sets up vault oidc provider
* @param {string} vaultAddr address of vault server
* @param {string} userName name of user to create in OIDC server
* @param {string} password password of user
* @param {string} email email of user
* @param {string} boundaryAddr address of boundary server for the redirect uri
* @returns {Promise<object>} OIDC info
*/
export async function setupVaultOidc(
vaultAddr,
userName,
password,
email,
boundaryAddr,
) {
const authPolicyName = 'auth-policy';
execSync(`vault auth enable userpass`);
execSync(
`vault policy write ${authPolicyName} ./admin/tests/fixtures/auth-policy.hcl`,
);
execSync(
`vault write auth/userpass/users/${userName} password=${password} token_policies=${authPolicyName} token_ttl=1h`,
);
execSync(
`vault write identity/entity name=${userName} metadata=email=${email} metadata=phone_number=123-456-7890 disabled=false`,
);
const entityId = execSync(
`vault read -field=id identity/entity/name/${userName}`,
)
.toString()
.trim();
const groupName = 'engineering';
execSync(
`vault write identity/group name=${groupName} member_entity_ids=${entityId}`,
);
const groupId = execSync(
`vault read -field=id identity/group/name/${groupName}`,
)
.toString()
.trim();
const authList = JSON.parse(
execSync(`vault auth list -detailed -format json`),
);
const userpassAccessor = authList['userpass/'].accessor;
execSync(
`vault write identity/entity-alias name=${userName} canonical_id=${entityId} mount_accessor=${userpassAccessor}`,
);
const assignmentName = 'my-assignment';
execSync(
`vault write identity/oidc/assignment/${assignmentName} entity_ids=${entityId} group_ids=${groupId}`,
);
const keyName = 'my-key';
execSync(
`vault write identity/oidc/key/${keyName} allowed_client_ids=* verification_ttl=2h rotation_period=1h algorithm=RS256`,
);
const oidcClientName = 'boundary';
execSync(
`vault write identity/oidc/client/${oidcClientName}` +
` redirect_uris=${boundaryAddr}/v1/auth-methods/oidc:authenticate:callback` +
` assignments=${assignmentName}` +
` key=${keyName}` +
` id_token_ttl=30m` +
` access_token_ttl=1h`,
);
const clientId = execSync(
`vault read -field=client_id identity/oidc/client/${oidcClientName}`,
)
.toString()
.trim();
const userScopeTemplate = `
{
"username": {{identity.entity.name}},
"email": {{identity.entity.metadata.email}},
"phone_number": {{identity.entity.metadata.phone_number}}
}`;
const userScopeEncoded = Buffer.from(userScopeTemplate).toString('base64');
execSync(`vault write identity/oidc/scope/user template=${userScopeEncoded}`);
const groupScopeTemplate = `
{
"groups": {{identity.entity.groups.names}}
}`;
const groupScopeEncoded = Buffer.from(groupScopeTemplate).toString('base64');
execSync(
`vault write identity/oidc/scope/groups template=${groupScopeEncoded}`,
);
const providerName = 'my-provider';
execSync(
`vault write identity/oidc/provider/${providerName} allowed_client_ids=${clientId} scopes_supported=groups,user issuer=${vaultAddr} &> /dev/null`,
);
const oidcConfig = JSON.parse(
execSync(
`curl -s ${vaultAddr}/v1/identity/oidc/provider/${providerName}/.well-known/openid-configuration`,
),
);
const issuer = oidcConfig.issuer;
const clientSecret = execSync(
`vault read -field=client_secret identity/oidc/client/${oidcClientName}`,
)
.toString()
.trim();
return {
issuer: issuer,
clientId: clientId,
clientSecret: clientSecret,
authPolicyName: authPolicyName,
};
}