-
Yash Rajpal authoredYash Rajpal authored
saml.spec.ts 17.09 KiB
import child_process from 'child_process';
import path from 'path';
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { v2 as compose } from 'docker-compose';
import { MongoClient } from 'mongodb';
import * as constants from './config/constants';
import { createUserFixture } from './fixtures/collections/users';
import { Users } from './fixtures/userStates';
import { Registration } from './page-objects';
import { convertHexToRGB } from './utils/convertHexToRGB';
import { createCustomRole, deleteCustomRole } from './utils/custom-role';
import { getUserInfo } from './utils/getUserInfo';
import { parseMeteorResponse } from './utils/parseMeteorResponse';
import { setSettingValueById } from './utils/setSettingValueById';
import type { BaseTest } from './utils/test';
import { test, expect } from './utils/test';
const resetTestData = async ({ api, cleanupOnly = false }: { api?: any; cleanupOnly?: boolean } = {}) => {
// Reset saml users' data on mongo in the beforeAll hook to allow re-running the tests within the same playwright session
// This is needed because those tests will modify this data and running them a second time would trigger different code paths
const connection = await MongoClient.connect(constants.URL_MONGODB);
const usernamesToDelete = [Users.userForSamlMerge, Users.userForSamlMerge2, Users.samluser1, Users.samluser2, Users.samluser4].map(
({ data: { username } }) => username,
);
await connection
.db()
.collection('users')
.deleteMany({
username: {
$in: usernamesToDelete,
},
});
if (cleanupOnly) {
return;
}
const usersFixtures = [Users.userForSamlMerge, Users.userForSamlMerge2].map((user) => createUserFixture(user));
await Promise.all(
usersFixtures.map((user) =>
connection.db().collection('users').updateOne({ username: user.username }, { $set: user }, { upsert: true }),
),
);
const settings = [
{ _id: 'Accounts_AllowAnonymousRead', value: false },
{ _id: 'SAML_Custom_Default_logout_behaviour', value: 'SAML' },
{ _id: 'SAML_Custom_Default_immutable_property', value: 'EMail' },
{ _id: 'SAML_Custom_Default_mail_overwrite', value: false },
{ _id: 'SAML_Custom_Default_name_overwrite', value: false },
{ _id: 'SAML_Custom_Default', value: false },
{ _id: 'SAML_Custom_Default_role_attribute_sync', value: true },
{ _id: 'SAML_Custom_Default_role_attribute_name', value: 'role' },
{ _id: 'SAML_Custom_Default_user_data_fieldmap', value: '{"username":"username", "email":"email", "name": "cn"}' },
{ _id: 'SAML_Custom_Default_provider', value: 'test-sp' },
{ _id: 'SAML_Custom_Default_issuer', value: 'http://localhost:3000/_saml/metadata/test-sp' },
{ _id: 'SAML_Custom_Default_entry_point', value: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php' },
{ _id: 'SAML_Custom_Default_idp_slo_redirect_url', value: 'http://localhost:8080/simplesaml/saml2/idp/SingleLogoutService.php' },
{ _id: 'SAML_Custom_Default_button_label_text', value: 'SAML test login button' },
{ _id: 'SAML_Custom_Default_button_color', value: '#185925' },
];
await Promise.all(settings.map(({ _id, value }) => setSettingValueById(api, _id, value)));
};
const setupCustomRole = async (api: BaseTest['api']) => {
const roleResponse = await createCustomRole(api, { name: 'saml-role' });
expect(roleResponse.status()).toBe(200);
const { role } = await roleResponse.json();
return role._id;
};
test.describe('SAML', () => {
let poRegistration: Registration;
let samlRoleId: string;
let targetInviteGroupId: string;
let targetInviteGroupName: string;
let inviteId: string;
const containerPath = path.join(__dirname, 'containers', 'saml');
test.beforeAll(async ({ api }) => {
await resetTestData({ api });
// Only one setting updated through the API to avoid refreshing the service configurations several times
await expect((await setSettingValueById(api, 'SAML_Custom_Default', true)).status()).toBe(200);
// Create a new custom role
if (constants.IS_EE) {
samlRoleId = await setupCustomRole(api);
}
await compose.buildOne('testsamlidp_idp', {
cwd: containerPath,
});
await compose.upOne('testsamlidp_idp', {
cwd: containerPath,
});
});
test.beforeAll(async ({ api }) => {
const groupResponse = await api.post('/groups.create', { name: faker.string.uuid() });
expect(groupResponse.status()).toBe(200);
const { group } = await groupResponse.json();
targetInviteGroupId = group._id;
targetInviteGroupName = group.name;
const inviteResponse = await api.post('/findOrCreateInvite', { rid: targetInviteGroupId, days: 1, maxUses: 0 });
expect(inviteResponse.status()).toBe(200);
const { _id } = await inviteResponse.json();
inviteId = _id;
});
test.afterAll(async ({ api }) => {
await compose.down({
cwd: containerPath,
});
// the compose CLI doesn't have any way to remove images, so try to remove it with a direct call to the docker cli, but ignore errors if it fails.
try {
child_process.spawn('docker', ['rmi', 'saml-testsamlidp_idp'], {
cwd: containerPath,
});
} catch {
// ignore errors here
}
// Remove saml test users so they don't interfere with other tests
await resetTestData({ cleanupOnly: true });
// Remove created custom role
if (constants.IS_EE) {
expect((await deleteCustomRole(api, 'saml-role')).status()).toBe(200);
}
});
test.afterAll(async ({ api }) => {
expect((await api.post('/groups.delete', { roomId: targetInviteGroupId })).status()).toBe(200);
});
test.beforeEach(async ({ page }) => {
poRegistration = new Registration(page);
await page.goto('/home');
});
test('Login', async ({ page, api }) => {
await test.step('expect to have SAML login button available', async () => {
await expect(poRegistration.btnLoginWithSaml).toBeVisible({ timeout: 10000 });
});
await test.step('expect to have SAML login button to have the required background color', async () => {
await expect(poRegistration.btnLoginWithSaml).toHaveCSS('background-color', convertHexToRGB('#185925'));
});
await test.step('expect to be redirected to the IdP for login', async () => {
await poRegistration.btnLoginWithSaml.click();
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
});
await test.step('expect to be redirected back on successful login', async () => {
await page.getByLabel('Username').fill('samluser1');
await page.getByLabel('Password').fill('password');
await page.locator('role=button[name="Login"]').click();
await expect(page).toHaveURL('/home');
});
await test.step('expect user data to have been mapped to the correct fields', async () => {
const user = await getUserInfo(api, 'samluser1');
expect(user).toBeDefined();
expect(user?.username).toBe('samluser1');
expect(user?.name).toBe('Saml User 1');
expect(user?.emails).toBeDefined();
expect(user?.emails?.[0].address).toBe('samluser1@example.com');
});
});
test('Allow password change for OAuth users', async ({ api }) => {
await test.step("should not send password reset mail if 'Allow Password Change for OAuth Users' setting is disabled", async () => {
expect((await setSettingValueById(api, 'Accounts_AllowPasswordChangeForOAuthUsers', false)).status()).toBe(200);
const response = await api.post('/method.call/sendForgotPasswordEmail', {
message: JSON.stringify({ msg: 'method', id: 'id', method: 'sendForgotPasswordEmail', params: ['samluser1@example.com'] }),
});
const mailSent = await parseMeteorResponse<boolean>(response);
expect(response.status()).toBe(200);
expect(mailSent).toBe(false);
});
await test.step("should send password reset mail if 'Allow Password Change for OAuth Users' setting is enabled", async () => {
expect((await setSettingValueById(api, 'Accounts_AllowPasswordChangeForOAuthUsers', true)).status()).toBe(200);
const response = await api.post('/method.call/sendForgotPasswordEmail', {
message: JSON.stringify({ msg: 'method', id: 'id', method: 'sendForgotPasswordEmail', params: ['samluser1@example.com'] }),
});
const mailSent = await parseMeteorResponse<boolean>(response);
expect(response.status()).toBe(200);
expect(mailSent).toBe(true);
});
});
const doLoginStep = async (page: Page, username: string, redirectUrl: string | null = '/home') => {
await test.step('expect successful login', async () => {
await poRegistration.btnLoginWithSaml.click();
// Redirect to Idp
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
// Fill username and password
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill('password');
await page.locator('role=button[name="Login"]').click();
// Redirect back to rocket.chat
if (redirectUrl) {
await expect(page).toHaveURL(redirectUrl);
await expect(page.getByRole('button', { name: 'User menu' })).toBeVisible();
}
});
};
const doLogoutStep = async (page: Page) => {
await test.step('logout', async () => {
await page.getByRole('button', { name: 'User menu' }).click();
await page.locator('//*[contains(@class, "rcx-option__content") and contains(text(), "Logout")]').click();
await expect(page).toHaveURL('/home');
await expect(page.getByRole('button', { name: 'User menu' })).not.toBeVisible();
});
};
test('Logout - Rocket.Chat only', async ({ page, api }) => {
await test.step('Configure logout to only logout from Rocket.Chat', async () => {
await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'Local')).status()).toBe(200);
});
await page.goto('/home');
await doLoginStep(page, 'samluser1');
await doLogoutStep(page);
await test.step('expect IdP to redirect back automatically on new login request', async () => {
await poRegistration.btnLoginWithSaml.click();
await expect(page).toHaveURL('/home');
});
});
test('Logout - Single Sign Out', async ({ page, api }) => {
await test.step('Configure logout to terminate SAML session', async () => {
await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'SAML')).status()).toBe(200);
});
await page.goto('/home');
await doLoginStep(page, 'samluser1');
await doLogoutStep(page);
await test.step('expect IdP to show login form on new login request', async () => {
await poRegistration.btnLoginWithSaml.click();
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
await expect(page.getByLabel('Username')).toBeVisible();
});
});
test('User Merge - By Email', async ({ page, api }) => {
await test.step('Configure SAML to identify users by email', async () => {
await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'EMail')).status()).toBe(200);
});
await doLoginStep(page, 'samluser2');
await test.step('expect user data to have been mapped to the correct fields', async () => {
const user = await getUserInfo(api, 'samluser2');
expect(user).toBeDefined();
expect(user?._id).toBe('user_for_saml_merge');
expect(user?.username).toBe('samluser2');
expect(user?.name).toBe('user_for_saml_merge');
expect(user?.emails).toBeDefined();
expect(user?.emails?.[0].address).toBe('user_for_saml_merge@email.com');
});
});
test('User Merge - By Email with Name Override', async ({ page, api }) => {
await test.step('Configure SAML to identify users by email', async () => {
expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'EMail')).status()).toBe(200);
expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', true)).status()).toBe(200);
});
await doLoginStep(page, 'samluser2');
await test.step('expect user data to have been mapped to the correct fields', async () => {
const user = await getUserInfo(api, 'samluser2');
expect(user).toBeDefined();
expect(user?._id).toBe('user_for_saml_merge');
expect(user?.username).toBe('samluser2');
expect(user?.name).toBe('Saml User 2');
expect(user?.emails).toBeDefined();
expect(user?.emails?.[0].address).toBe('user_for_saml_merge@email.com');
});
});
test('User Merge - By Username', async ({ page, api }) => {
await test.step('Configure SAML to identify users by username', async () => {
expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', false)).status()).toBe(200);
expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', false)).status()).toBe(200);
});
await doLoginStep(page, 'samluser3');
await test.step('expect user data to have been mapped to the correct fields', async () => {
const user = await getUserInfo(api, 'user_for_saml_merge2');
expect(user).toBeDefined();
expect(user?._id).toBe('user_for_saml_merge2');
expect(user?.username).toBe('user_for_saml_merge2');
expect(user?.name).toBe('user_for_saml_merge2');
expect(user?.emails).toBeDefined();
expect(user?.emails?.[0].address).toBe('user_for_saml_merge2@email.com');
});
});
test('User Merge - By Username with Email Override', async ({ page, api }) => {
await test.step('Configure SAML to identify users by username', async () => {
expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', false)).status()).toBe(200);
expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', true)).status()).toBe(200);
});
await doLoginStep(page, 'samluser3');
await test.step('expect user data to have been mapped to the correct fields', async () => {
const user = await getUserInfo(api, 'user_for_saml_merge2');
expect(user).toBeDefined();
expect(user?._id).toBe('user_for_saml_merge2');
expect(user?.username).toBe('user_for_saml_merge2');
expect(user?.name).toBe('user_for_saml_merge2');
expect(user?.emails).toBeDefined();
expect(user?.emails?.[0].address).toBe('samluser3@example.com');
});
});
test('User Merge - By Username with Name Override', async ({ page, api }) => {
await test.step('Configure SAML to identify users by username', async () => {
await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
await expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', true)).status()).toBe(200);
});
await doLoginStep(page, 'samluser3');
await test.step('expect user data to have been mapped to the correct fields', async () => {
const user = await getUserInfo(api, 'user_for_saml_merge2');
expect(user).toBeDefined();
expect(user?._id).toBe('user_for_saml_merge2');
expect(user?.username).toBe('user_for_saml_merge2');
expect(user?.name).toBe('Saml User 3');
expect(user?.emails).toBeDefined();
expect(user?.emails?.[0].address).toBe('samluser3@example.com');
});
});
test('User Mapping - Custom Role', async ({ page, api }) => {
test.skip(!constants.IS_EE);
await doLoginStep(page, 'samluser4');
await test.step('expect users role to have been mapped correctly', async () => {
const user = await getUserInfo(api, 'samluser4');
expect(user).toBeDefined();
expect(user?.username).toBe('samluser4');
expect(user?.name).toBe('Saml User 4');
expect(user?.emails).toBeDefined();
expect(user?.emails?.[0].address).toBe('samluser4@example.com');
expect(user?.roles).toBeDefined();
expect(user?.roles?.length).toBe(1);
expect(user?.roles).toContain(samlRoleId);
});
});
test('Redirect to a specific group after login when using a valid invite link', async ({ page }) => {
await page.goto(`/invite/${inviteId}`);
await page.getByRole('link', { name: 'Back to Login' }).click();
await doLoginStep(page, 'samluser1', null);
await test.step('expect to be redirected to the invited room after succesful login', async () => {
await expect(page).toHaveURL(`/group/${targetInviteGroupName}`);
});
});
test('Redirect to home after login when no redirectUrl is provided', async ({ page }) => {
await doLoginStep(page, 'samluser2');
await test.step('expect to be redirected to the homepage after succesful login', async () => {
await expect(page).toHaveURL('/home');
});
});
test.fixme('User Merge - By Custom Identifier', async () => {
// Test user merge with a custom identifier configured in the fieldmap
});
test.fixme('Signature Validation', async () => {
// Test login with signed responses
});
test.fixme('Login - User without username', async () => {
// Test login with a SAML user with no username
// Test different variations of the Immutable Property setting
});
test.fixme('Login - User without email', async () => {
// Test login with a SAML user with no email
// Test different variations of the Immutable Property setting
});
test.fixme('Login - User without name', async () => {
// Test login with a SAML user with no name
});
test.fixme('Login - User with channels attribute', async () => {
// Test login with a SAML user with a "channels" attribute
});
test.fixme('Data Sync - Custom Field Map', async () => {
// Test the data sync using a custom fieldmap setting
});
});