Skip to content

Commit 94b0f89

Browse files
committed
[MS] Async enrollment test
1 parent ca6e313 commit 94b0f89

9 files changed

Lines changed: 390 additions & 204 deletions

File tree

client/src/parsec/async_enrollment.ts

Lines changed: 119 additions & 198 deletions
Large diffs are not rendered by default.

client/src/parsec/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export {
7373
ShowCertificateSelectionDialogErrorTag,
7474
SubmitAsyncEnrollmentErrorTag,
7575
SubmitAsyncEnrollmentIdentityStrategyTag,
76+
SubmitterCancelAsyncEnrollmentErrorTag,
77+
SubmitterFinalizeAsyncEnrollmentErrorTag,
7678
UpdateDeviceErrorTag,
7779
UserOnlineStatus,
7880
UserProfile,
@@ -100,6 +102,7 @@ export {
100102
WorkspaceStatEntryErrorTag,
101103
WorkspaceStatFolderChildrenErrorTag,
102104
WorkspaceStopErrorTag,
105+
X509URIFlavorValueTag,
103106
} from '@/plugins/libparsec';
104107
export type {
105108
AcceptFinalizeAsyncEnrollmentIdentityStrategy,
@@ -128,10 +131,12 @@ export type {
128131
AnyClaimRetrievedInfoUser,
129132
ApiVersion,
130133
ArchiveDeviceError,
134+
AsyncEnrollmentIdentitySystem,
131135
AsyncEnrollmentUntrusted,
132136
AvailableDevice,
133137
AvailableDeviceType,
134138
AvailableDeviceTypeOpenBao,
139+
AvailablePendingAsyncEnrollmentIdentitySystem,
135140
AvailablePendingAsyncEnrollmentIdentitySystemPKI,
136141
BootstrapOrganizationError,
137142
ClaimerRetrieveInfoError,

client/src/parsec/workspace.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,11 @@ export async function initializeWorkspace(
4242
): Promise<Result<StartedWorkspaceInfo, WorkspaceInfoError | ClientStartWorkspaceError>> {
4343
const startResult = await startWorkspace(workspaceId, connectionHandle);
4444
if (!startResult.ok) {
45-
console.error(`Failed to start workspace ${workspaceId}: ${startResult.error}`);
4645
return startResult;
4746
}
4847

4948
const startedWorkspaceResult = await getWorkspaceInfo(startResult.value);
5049
if (!startedWorkspaceResult.ok) {
51-
console.error(`Failed to get started workspace info ${workspaceId}: ${startedWorkspaceResult.error}`);
5250
return startedWorkspaceResult;
5351
}
5452
return startedWorkspaceResult;
@@ -69,7 +67,6 @@ export async function listWorkspaces(
6967
for (const wkInfo of result.value) {
7068
const startResult = await startWorkspace(wkInfo.id, handle);
7169
if (!startResult.ok) {
72-
console.error(`Failed to start workspace ${wkInfo.currentName}: ${startResult.error}`);
7370
continue;
7471
}
7572

@@ -239,7 +236,6 @@ export async function mountWorkspace(
239236
): Promise<Result<[MountpointHandle, SystemPath], WorkspaceMountError>> {
240237
const startedWorkspaceResult = await getWorkspaceInfo(workspaceHandle);
241238
if (!startedWorkspaceResult.ok) {
242-
console.error(`Failed to get started workspace info: ${startedWorkspaceResult.error}`);
243239
return { ok: false, error: { tag: WorkspaceMountErrorTag.Internal, error: startedWorkspaceResult.error.error } };
244240
} else {
245241
if (startedWorkspaceResult.value.mountpoints.length > 0) {

client/src/views/home/HomePage.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ async function handleAsyncEnrollment(link: string): Promise<void> {
483483
serverConfig: serverConfigResult.ok ? serverConfigResult.value : undefined,
484484
pkiAvailable: pkiAvailable,
485485
},
486-
cssClass: 'join-async-modal',
486+
cssClass: 'async-enrollment-modal',
487487
});
488488
await modal.present();
489489
const { role } = await modal.onDidDismiss();

client/src/views/users/AsyncEnrollmentModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ async function onNextButtonClicked(): Promise<boolean> {
335335
}
336336
337337
async function onPreviousClicked(): Promise<boolean> {
338-
if (state.value === JoinRequestState.ChooseMethod) {
338+
if (state.value === JoinRequestState.ChooseMethod || !multipleMethodsAvailable.value) {
339339
return modalController.dismiss(null, MsModalResult.Cancel);
340340
} else {
341341
state.value = JoinRequestState.ChooseMethod;

client/src/views/users/BulkRoleAssignmentModal.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ async function findWorkspaces(): Promise<void> {
252252
253253
const sourceWorkspaces = sourceUserWorkspacesResult.value;
254254
const targetWorkspaces = targetUserWorkspacesResult.value;
255+
255256
roleUpdates.value = [];
256257
257258
for (const wsInfo of sourceWorkspaces) {

client/tests/e2e/helpers/fixtures.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export async function setupNewPage(page: MsPage, opts: SetupOptions = {}): Promi
6868
if (options.enableUpdateEvent) {
6969
(window as any).TESTING_ENABLE_UPDATE_EVENT = options.enableUpdateEvent;
7070
}
71+
if (options.mockPki) {
72+
(window as any).TESTING_PKI = true;
73+
}
7174
if (options.openBaoServer) {
7275
(window as any).TESTING_OPEN_BAO_SERVER = options.openBaoServer;
7376
}

client/tests/e2e/helpers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface SetupOptions {
2828
libparsecMockFunctions?: Array<LibParsecFunction>;
2929
enableStripe?: boolean;
3030
enableUpdateEvent?: boolean;
31+
mockPki?: boolean;
3132
}
3233

3334
export interface MsPage extends Page {
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
2+
3+
import {
4+
answerQuestion,
5+
expect,
6+
fillInputModal,
7+
fillIonInput,
8+
getOrganizationAddr,
9+
login,
10+
logout,
11+
MsPage,
12+
msTest,
13+
setupNewPage,
14+
} from '@tests/e2e/helpers';
15+
16+
function getAsyncEnrollmentJoinLink(orgName: string): string {
17+
const orgAddr = getOrganizationAddr(orgName);
18+
if (orgAddr.includes('?')) {
19+
return `${orgAddr}&a=async_enrollment`;
20+
} else {
21+
return `${orgAddr}?a=async_enrollment`;
22+
}
23+
}
24+
25+
async function addRequest(page: MsPage, identitySystem: 'pki' | 'openbao'): Promise<void> {
26+
const requests = page.locator('.organization-request');
27+
await expect(requests).toBeHidden();
28+
const orgName = await page.locator('.organization-card').first().locator('.organization-name').textContent();
29+
expect(orgName).not.toBeNull();
30+
const link = getAsyncEnrollmentJoinLink(orgName as string);
31+
await page.locator('#create-organization-button').click();
32+
await expect(page.locator('.homepage-popover')).toBeVisible();
33+
await page.locator('.homepage-popover').getByRole('listitem').nth(1).click();
34+
await fillInputModal(page, link);
35+
const requestModal = page.locator('.async-enrollment-modal');
36+
await expect(requestModal).toBeVisible();
37+
const nextButton = requestModal.locator('#next-button');
38+
const previousButton = requestModal.locator('#cancel-button');
39+
40+
await expect(requestModal.locator('.ms-modal-header__title')).toHaveText(`Join organization ${orgName}`);
41+
await expect(nextButton).toBeTrulyDisabled();
42+
await expect(nextButton).toHaveText('Continue');
43+
await expect(previousButton).toHaveText('Cancel');
44+
45+
if (identitySystem === 'pki') {
46+
await expect(requestModal.locator('.choose-method')).toBeVisible();
47+
const options = requestModal.locator('.choose-method').locator('.choose-method-options-item');
48+
await expect(options.locator('.title-h4')).toHaveText(['Continue with PKI', 'Continue with Single Sign-On']);
49+
await options.nth(0).click();
50+
await expect(nextButton).toBeTrulyEnabled();
51+
await nextButton.click();
52+
}
53+
await expect(requestModal.locator('.modal-info')).toBeVisible();
54+
55+
if (identitySystem === 'pki') {
56+
await expect(requestModal.locator('.async-authentication-modal-header__title')).toHaveText('Certificate Authentication (PKI)');
57+
await requestModal.locator('.choose-certificate-button').click();
58+
} else {
59+
await expect(requestModal.locator('.async-authentication-modal-header__title')).toHaveText(
60+
'Log in with SSO to access the organization.',
61+
);
62+
await requestModal.locator('.proconnect-button').click();
63+
await expect(requestModal.locator('.modal-info')).toBeHidden();
64+
await expect(requestModal.locator('.user-information')).toBeVisible();
65+
await fillIonInput(requestModal.locator('.user-information').locator('ion-input'), 'Gordon Freeman');
66+
await expect(requestModal.locator('.user-information').locator('.dropdown-button-content')).toHaveText(/^[a-f0-9-]+@example\.invalid$/);
67+
}
68+
await expect(nextButton).toBeTrulyEnabled();
69+
await nextButton.click();
70+
await expect(page).toShowToast('Your request to join the organization has been sent.', 'Success');
71+
await expect(requestModal).toBeHidden();
72+
await expect(requests).toHaveCount(1);
73+
await expect(requests.locator('.organization-request-organization')).toHaveText(orgName as string);
74+
await expect(requests.locator('.organization-request-username')).toHaveText('Gordon Freeman');
75+
await expect(requests.locator('.organization-request-status')).toHaveText('Pending');
76+
}
77+
78+
for (const identitySystem of ['pki', 'openbao']) {
79+
msTest(`Async enrollment using ${identitySystem}`, async ({ context }) => {
80+
const page = (await context.newPage()) as MsPage;
81+
await setupNewPage(page, { mockPki: identitySystem === 'pki' });
82+
83+
await addRequest(page, identitySystem as 'pki' | 'openbao');
84+
85+
const requests = page.locator('.organization-request');
86+
87+
await login(page, 'Alicey McAliceFace');
88+
89+
await page.locator('.sidebar').locator('#sidebar-invitations').click();
90+
await expect(page).toHavePageTitle('Invitations & Requests');
91+
92+
await expect(page.locator('.toggle-view-container').locator('.pki-button').locator('.toggle-view-button__label')).toHaveText(
93+
'Link requests',
94+
);
95+
await expect(page.locator('.toggle-view-container').locator('.pki-button').locator('.toggle-view-button__count')).toHaveText('1');
96+
await page.locator('.toggle-view-container').locator('.pki-button').click();
97+
98+
const linkRequests = page.locator('.invitations-container').locator('.request-list-item');
99+
await expect(linkRequests).toHaveCount(1);
100+
await expect(linkRequests.nth(0).locator('.request-type__label')).toHaveText(identitySystem === 'pki' ? 'PKI' : 'SSO');
101+
await expect(linkRequests.nth(0).locator('.person-name')).toHaveText('Gordon Freeman');
102+
await expect(linkRequests.nth(0).locator('.request-email__label')).toHaveText(
103+
identitySystem === 'pki' ? 'gordon.freeman@blackmesa.nm' : /^ [a-f0-9-]+@example\.invalid$/,
104+
);
105+
106+
const acceptButton = linkRequests.nth(0).locator('.request-actions').locator('ion-button').nth(0);
107+
await expect(acceptButton).toHaveText('Accept');
108+
const acceptModal = page.locator(identitySystem === 'pki' ? '.async-enrollment-pki-modal' : '.async-enrollment-openbao-modal');
109+
await expect(acceptModal).toBeHidden();
110+
await acceptButton.click();
111+
await expect(acceptModal).toBeVisible();
112+
113+
if (identitySystem === 'pki') {
114+
await expect(acceptModal.locator('.async-authentication-modal-header__title')).toHaveText(
115+
'A certificate is required to accept the request',
116+
);
117+
await expect(acceptModal.locator('.async-authentication-modal-text')).toHaveText(
118+
'A certificate is required to validate requests received by PKI. ' +
119+
'For security reasons, it must be provided each time the organization is reopened.',
120+
);
121+
await acceptModal.locator('.choose-certificate-button').click();
122+
} else {
123+
await acceptModal.locator('.proconnect-button').click();
124+
await expect(acceptModal.locator('.proconnect-group--connected')).toBeVisible();
125+
await expect(acceptModal.locator('.proconnect-group--connected')).toHaveText('Connected');
126+
await acceptModal.locator('#next-button').click();
127+
}
128+
await expect(acceptModal).toBeHidden();
129+
const selectProfileModal = page.locator('.select-profile-modal');
130+
await expect(selectProfileModal).toBeVisible();
131+
await expect(selectProfileModal.locator('.user-details').locator('ion-input').nth(0).locator('input')).toHaveValue('Gordon Freeman');
132+
await expect(selectProfileModal.locator('.user-details').locator('ion-input').nth(1).locator('input')).toHaveValue(
133+
identitySystem === 'pki' ? 'gordon.freeman@blackmesa.nm' : /^[a-f0-9-]+@example\.invalid$/,
134+
);
135+
await expect(selectProfileModal.locator('#dropdown-popover-button')).toHaveText('Select a profile');
136+
await expect(selectProfileModal.locator('#next-button')).toHaveText('Validate request');
137+
await expect(selectProfileModal.locator('#next-button')).toBeTrulyDisabled();
138+
const profilePopover = page.locator('.dropdown-popover');
139+
await expect(profilePopover).toBeHidden();
140+
await selectProfileModal.locator('#dropdown-popover-button').click();
141+
await expect(profilePopover).toBeVisible();
142+
await expect(profilePopover.locator('.option').locator('.option-text__label')).toHaveText(['Administrator', 'Member', 'External']);
143+
await profilePopover.locator('.option').nth(1).click();
144+
await expect(selectProfileModal.locator('#next-button')).toBeTrulyEnabled();
145+
await selectProfileModal.locator('#next-button').click();
146+
await expect(page).toShowToast('This request has been accepted', 'Success');
147+
148+
await logout(page);
149+
150+
await expect(requests).toHaveCount(1);
151+
await expect(requests.nth(0).locator('.organization-request-button')).toBeVisible();
152+
await expect(requests.nth(0).locator('.organization-request-button')).toHaveText('Add to my organizations');
153+
await requests.nth(0).locator('.organization-request-button').click();
154+
155+
if (identitySystem !== 'pki') {
156+
const finalizeModal = page.locator('.async-enrollment-openbao-modal');
157+
await expect(finalizeModal).toBeVisible();
158+
await finalizeModal.locator('.proconnect-button').click();
159+
await expect(finalizeModal.locator('#next-button')).toBeVisible();
160+
await finalizeModal.locator('#next-button').click();
161+
await expect(page).toShowToast('You successfully joined the organization.', 'Success');
162+
await expect(page).toBeWorkspacePage();
163+
} else {
164+
// Using a fake device so we can't log in, and testing for toast causes problems because two toasts
165+
// are opened one after the other.
166+
await expect(requests).toHaveCount(0);
167+
}
168+
169+
await page.release();
170+
});
171+
172+
msTest(`Cancel request using ${identitySystem}`, async ({ context }) => {
173+
const page = (await context.newPage()) as MsPage;
174+
await setupNewPage(page, { mockPki: identitySystem === 'pki' });
175+
176+
await addRequest(page, identitySystem as 'pki' | 'openbao');
177+
178+
const requests = page.locator('.organization-request');
179+
180+
await expect(requests).toHaveCount(1);
181+
await requests.nth(0).locator('.organization-request-icon').click();
182+
await answerQuestion(page, true, {
183+
expectedNegativeText: 'No, wait for validation',
184+
expectedPositiveText: 'Cancel request',
185+
expectedQuestionText: 'Your request to join the organization is still pending. Are you sure you want to cancel it?',
186+
expectedTitleText: 'Organization request pending',
187+
});
188+
await expect(requests).toHaveCount(0);
189+
190+
// Check that the admin doesn't see the request
191+
await login(page, 'Alicey McAliceFace');
192+
193+
await page.locator('.sidebar').locator('#sidebar-invitations').click();
194+
await expect(page).toHavePageTitle('Invitations & Requests');
195+
196+
await expect(page.locator('.toggle-view-container').locator('.pki-button').locator('.toggle-view-button__label')).toHaveText(
197+
'Link requests',
198+
);
199+
await expect(page.locator('.toggle-view-container').locator('.pki-button').locator('.toggle-view-button__count')).toHaveText('0');
200+
await page.locator('.toggle-view-container').locator('.pki-button').click();
201+
202+
const linkRequests = page.locator('.invitations-container').locator('.request-list-item');
203+
await expect(linkRequests).toHaveCount(0);
204+
205+
await expect(page.locator('.invitations-container').locator('.no-active-content')).toBeVisible();
206+
await expect(page.locator('.invitations-container').locator('.no-active-content')).toHaveText(
207+
'No pending link requests. These requests are for PKI or SSO users.',
208+
);
209+
210+
await page.release();
211+
});
212+
213+
msTest(`Reject async enrollment using ${identitySystem}`, async ({ context }) => {
214+
const page = (await context.newPage()) as MsPage;
215+
await setupNewPage(page, { mockPki: identitySystem === 'pki' });
216+
217+
await addRequest(page, identitySystem as 'pki' | 'openbao');
218+
219+
const requests = page.locator('.organization-request');
220+
221+
await login(page, 'Alicey McAliceFace');
222+
223+
await page.locator('.sidebar').locator('#sidebar-invitations').click();
224+
await expect(page).toHavePageTitle('Invitations & Requests');
225+
226+
await expect(page.locator('.toggle-view-container').locator('.pki-button').locator('.toggle-view-button__label')).toHaveText(
227+
'Link requests',
228+
);
229+
await expect(page.locator('.toggle-view-container').locator('.pki-button').locator('.toggle-view-button__count')).toHaveText('1');
230+
await page.locator('.toggle-view-container').locator('.pki-button').click();
231+
232+
const linkRequests = page.locator('.invitations-container').locator('.request-list-item');
233+
await expect(linkRequests).toHaveCount(1);
234+
await expect(linkRequests.nth(0).locator('.request-type__label')).toHaveText(identitySystem === 'pki' ? 'PKI' : 'SSO');
235+
await expect(linkRequests.nth(0).locator('.person-name')).toHaveText('Gordon Freeman');
236+
await expect(linkRequests.nth(0).locator('.request-email__label')).toHaveText(
237+
identitySystem === 'pki' ? 'gordon.freeman@blackmesa.nm' : /^ [a-f0-9-]+@example\.invalid$/,
238+
);
239+
240+
const rejectButton = linkRequests.nth(0).locator('.request-actions').locator('ion-button').nth(1);
241+
await rejectButton.click();
242+
await expect(page).toShowToast('This request has been rejected', 'Info');
243+
await expect(linkRequests).toHaveCount(0);
244+
await expect(page.locator('.invitations-container').locator('.no-active-content')).toBeVisible();
245+
await expect(page.locator('.invitations-container').locator('.no-active-content')).toHaveText(
246+
'No pending link requests. These requests are for PKI or SSO users.',
247+
);
248+
249+
await logout(page);
250+
251+
await expect(requests).toHaveCount(1);
252+
await expect(requests.nth(0).locator('.organization-request-status')).toHaveText('Rejected');
253+
await requests.nth(0).locator('.organization-request-icon').click();
254+
await expect(page).toShowToast('Your request to join the organization has been deleted.', 'Info');
255+
await expect(requests).toHaveCount(0);
256+
257+
await page.release();
258+
});
259+
}

0 commit comments

Comments
 (0)