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

upcoming: [M3-7463] - Disable Billing access user permission for child users #10045

Merged
merged 9 commits into from
Jan 10, 2024
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))
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