Skip to content

Commit

Permalink
upcoming: [M3-7463] - Disable Billing access user permission for chil…
Browse files Browse the repository at this point in the history
…d users (#10045)

* Disable billing access perm for child users

* Check the correct perm if account_access is restricted

* Fix typo and add changeset

* Fix issue where RO was still selected when None was clicked

* Add WIP test to user-permissions.spec]

* Fix test coverage, thanks @jdamore-linode

* Added changeset: Add test coverage for Billing Access perms for Child users

* Remove ramda on selection check and fix Cypress failure

* Make changeset wording consistent because it was bothering me
  • Loading branch information
mjac0bs authored Jan 10, 2024
1 parent 7a104dc commit 81174ef
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 4 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10045-tests-1704834823931.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Add test coverage for Billing Access permission for Child accounts ([#10045](https://github.com/linode/manager/pull/10045))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Disable Billing Access user permission for Child accounts ([#10045](https://github.com/linode/manager/pull/10045))
90 changes: 88 additions & 2 deletions packages/manager/cypress/e2e/core/account/user-permissions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Grant, Grants } from '@linode/api-v4';
import { profileFactory } from '@src/factories';
import { accountUserFactory } from '@src/factories/accountUsers';
import { grantsFactory } from '@src/factories/grants';
import { userPermissionsGrants } from 'support/constants/user-permissions';
Expand All @@ -9,8 +10,14 @@ import {
mockUpdateUser,
mockUpdateUserGrants,
} from 'support/intercepts/account';
import {
mockAppendFeatureFlags,
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import { mockGetProfile } from 'support/intercepts/profile';
import { ui } from 'support/ui';
import { shuffleArray } from 'support/util/arrays';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import { randomLabel } from 'support/util/random';

// Message shown when user has unrestricted account acess.
Expand Down Expand Up @@ -222,8 +229,9 @@ describe('User permission management', () => {

ui.toast.assertMessage('User permissions successfully saved.');

// Smoke tests to confirm that "Global Permissions" and "Specific Permissions"
// Smoke tests to confirm that "General Permissions" and "Specific Permissions"
// sections are visible.
cy.findByText('General Permissions').should('be.visible');
cy.findByText(unrestrictedAccessMessage).should('not.exist');
cy.get('[data-qa-global-section]')
.should('be.visible')
Expand Down Expand Up @@ -253,8 +261,8 @@ describe('User permission management', () => {
.should('be.visible')
.click();

cy.findByText('General Permissions').should('be.visible');
cy.findByText(unrestrictedAccessMessage).should('be.visible');
cy.findByText('Global Permissions').should('not.exist');
cy.findByText('Billing Access').should('not.exist');
cy.findByText('Specific Permissions').should('not.exist');
});
Expand Down Expand Up @@ -472,4 +480,82 @@ describe('User permission management', () => {
});
});
});

it('disables Read-Write and defaults to Read Only Billing Access for child account users with Parent/Child feature flag', () => {
const mockProfile = profileFactory.build({
username: 'unrestricted-child-user',
});

const mockActiveUser = accountUserFactory.build({
username: 'unrestricted-child-user',
restricted: false,
user_type: 'child',
});

const mockRestrictedUser = {
...mockActiveUser,
restricted: true,
username: 'restricted-child-user',
};

const mockUserGrants = grantsFactory.build({
global: { account_access: 'read_write' },
});

// TODO: Parent/Child - M3-7559 clean up when feature is live in prod and feature flag is removed.
mockAppendFeatureFlags({
parentChildAccountAccess: makeFeatureFlagData(true),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientStream');

mockGetUsers([mockActiveUser, mockRestrictedUser]).as('getUsers');
mockGetUser(mockActiveUser);
mockGetUserGrants(mockActiveUser.username, mockUserGrants);
mockGetProfile(mockProfile);

// Navigate to Users & Grants page, find mock restricted user, click its "User Permissions" button.
cy.visitWithLogin('/account/users');
cy.wait('@getUsers');
cy.findByText(mockRestrictedUser.username)
.should('be.visible')
.closest('tr')
.within(() => {
ui.button
.findByTitle('User Permissions')
.should('be.visible')
.should('be.enabled')
.click();
});

cy.visitWithLogin(
`/account/users/${mockRestrictedUser.username}/permissions`
);
mockGetUser(mockRestrictedUser);
mockGetUserGrants(mockRestrictedUser.username, mockUserGrants);
cy.wait(['@getClientStream', '@getFeatureFlags']);

cy.get('[data-qa-global-section]')
.should('be.visible')
.within(() => {
// Confirm that 'Read-Write' Billing Access is disabled and 'Read Only' Billing Access is selected by default.
cy.get(`[data-qa-select-card-heading="Read-Write"]`)
.closest('[data-qa-selection-card]')
.should('be.visible')
.should('have.attr', 'disabled');
assertBillingAccessSelected('Read Only');

// Switch billing access to "None" and confirm that "Read Only" has been deselected.
selectBillingAccess('None');
cy.get(`[data-qa-select-card-heading="Read Only"]`)
.closest('[data-qa-selection-card]')
.should('be.visible')
.should('have.attr', 'data-qa-selection-card-checked', 'false');

ui.button
.findByTitle('Save')
.should('be.visible')
.should('be.enabled')
.click();
});
});
});
24 changes: 22 additions & 2 deletions packages/manager/src/features/Users/UserPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ interface State {
childAccountAccessEnabled: boolean;
errors?: APIError[];
grants?: Grants;
isAccountAccessRestricted: boolean;
isSavingEntity: boolean;
isSavingGlobal: boolean;
loading: boolean;
Expand Down Expand Up @@ -156,18 +157,28 @@ class UserPermissions extends React.Component<CombinedProps, State> {

checkAndEnableChildAccountAccess = async () => {
const { currentUser: currentUsername, flags } = this.props;

// Current user is the active user on the account.
if (currentUsername) {
try {
const currentUser = await getUser(currentUsername);

const isChildAccount = currentUser.user_type === 'child';
const isParentAccount = currentUser.user_type === 'parent';
const isFeatureFlagOn = flags.parentChildAccountAccess;

// A parent user account should have a toggleable `child_account_access` grant for its restricted users.
this.setState({
childAccountAccessEnabled: Boolean(
isParentAccount && isFeatureFlagOn
),
});

// A child user account should have no more than `read_only` billing (account) access.
// Since API returns `read_write` for child users' `account_access` grant, we must manually disable the `read_write` Billing Access permission.
this.setState({
isAccountAccessRestricted: Boolean(isChildAccount && isFeatureFlagOn),
});
} catch (error) {
this.setState({
errors: getAPIErrorOrDefault(
Expand Down Expand Up @@ -394,18 +405,26 @@ class UserPermissions extends React.Component<CombinedProps, State> {
subheadings={['The user cannot view any billing information.']}
/>
<SelectionCard
checked={grants.global.account_access === 'read_only'}
checked={
grants.global.account_access === 'read_only' ||
(this.state.isAccountAccessRestricted &&
Boolean(this.state.grants?.global.account_access))
}
data-qa-billing-access="Read Only"
heading="Read Only"
onClick={this.billingPermOnClick('read_only')}
subheadings={['Can view invoices and billing info.']}
/>
<SelectionCard
checked={
grants.global.account_access === 'read_write' &&
!this.state.isAccountAccessRestricted
}
subheadings={[
'Can make payments, update contact and billing info, and will receive copies of all invoices and payment emails.',
]}
checked={grants.global.account_access === 'read_write'}
data-qa-billing-access="Read-Write"
disabled={this.state.isAccountAccessRestricted}
heading="Read-Write"
onClick={this.billingPermOnClick('read_write')}
/>
Expand Down Expand Up @@ -805,6 +824,7 @@ class UserPermissions extends React.Component<CombinedProps, State> {

state: State = {
childAccountAccessEnabled: false,
isAccountAccessRestricted: false,
isSavingEntity: false,
isSavingGlobal: false,
loading: true,
Expand Down

0 comments on commit 81174ef

Please sign in to comment.