Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-12654-changed-1754641803662.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

IAM permissions for billing payment methods ([#12654](https://github.com/linode/manager/pull/12654))
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,26 @@ const props = {
paymentMethod: undefined,
};

const queryMocks = vi.hoisted(() => ({
userPermissions: vi.fn(() => ({
data: {
delete_payment_method: false,
},
})),
}));

vi.mock('src/features/IAM/hooks/usePermissions', () => ({
usePermissions: queryMocks.userPermissions,
}));

describe('Delete Payment Method Dialog', () => {
it('renders the delete payment method dialog', () => {
queryMocks.userPermissions.mockReturnValue({
data: {
delete_payment_method: true,
},
});

const screen = renderWithTheme(<DeletePaymentMethodDialog {...props} />);

const headerText = screen.getByText('Delete Payment Method');
Expand All @@ -31,6 +49,12 @@ describe('Delete Payment Method Dialog', () => {
});

it('calls the corresponding functions when buttons are clicked', () => {
queryMocks.userPermissions.mockReturnValue({
data: {
delete_payment_method: true,
},
});

const screen = renderWithTheme(<DeletePaymentMethodDialog {...props} />);

const deleteButton = screen.getByText('Delete');
Expand All @@ -43,4 +67,17 @@ describe('Delete Payment Method Dialog', () => {
fireEvent.click(cancelButton);
expect(props.onClose).toHaveBeenCalled();
});

it('disables the delete button if the user does not have the delete_payment_method permission', () => {
queryMocks.userPermissions.mockReturnValue({
data: {
delete_payment_method: false,
},
});

const screen = renderWithTheme(<DeletePaymentMethodDialog {...props} />);

const deleteButton = screen.getByText('Delete');
expect(deleteButton).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { makeStyles } from 'tss-react/mui';

import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
import CreditCard from 'src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/CreditCard';
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';

import { ThirdPartyPayment } from './ThirdPartyPayment';

Expand Down Expand Up @@ -35,11 +36,16 @@ export const DeletePaymentMethodDialog = React.memo((props: Props) => {
const { error, loading, onClose, onDelete, open, paymentMethod } = props;
const { classes } = useStyles();

const { data: permissions } = usePermissions('account', [
'delete_payment_method',
]);

const actions = (
<ActionsPanel
primaryButtonProps={{
label: 'Delete',
loading,
disabled: !permissions?.delete_payment_method,
onClick: onDelete,
}}
secondaryButtonProps={{ label: 'Cancel', onClick: onClose }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const queryMocks = vi.hoisted(() => ({
userPermissions: vi.fn(() => ({
data: {
make_billing_payment: false,
update_account: false,
set_default_payment_method: false,
delete_payment_method: false,
},
})),
}));
Expand Down Expand Up @@ -148,7 +149,8 @@ describe('Payment Method Row', () => {
queryMocks.userPermissions.mockReturnValue({
data: {
make_billing_payment: false,
update_account: true,
set_default_payment_method: false,
delete_payment_method: true,
},
});
const { getByLabelText, getByText } = renderWithTheme(
Expand All @@ -174,7 +176,8 @@ describe('Payment Method Row', () => {
queryMocks.userPermissions.mockReturnValue({
data: {
make_billing_payment: true,
update_account: true,
set_default_payment_method: true,
delete_payment_method: false,
},
});
const paymentMethod = paymentMethodFactory.build({
Expand Down Expand Up @@ -205,7 +208,8 @@ describe('Payment Method Row', () => {
queryMocks.userPermissions.mockReturnValue({
data: {
make_billing_payment: false,
update_account: false,
set_default_payment_method: false,
delete_payment_method: false,
},
});
const { getByLabelText, getByText } = renderWithTheme(
Expand All @@ -231,7 +235,8 @@ describe('Payment Method Row', () => {
queryMocks.userPermissions.mockReturnValue({
data: {
make_billing_payment: true,
update_account: false,
set_default_payment_method: false,
delete_payment_method: false,
},
});
const { getByLabelText, getByText } = renderWithTheme(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export const PaymentMethodRow = (props: Props) => {

const { data: permissions } = usePermissions('account', [
'make_billing_payment',
'update_account',
'set_default_payment_method',
'delete_payment_method',
]);

const makeDefault = () => {
Expand Down Expand Up @@ -74,7 +75,9 @@ export const PaymentMethodRow = (props: Props) => {
},
{
disabled:
isChildUser || !permissions.update_account || paymentMethod.is_default,
isChildUser ||
!permissions.set_default_payment_method ||
paymentMethod.is_default,
onClick: makeDefault,
title: 'Make Default',
tooltip: paymentMethod.is_default
Expand All @@ -83,7 +86,9 @@ export const PaymentMethodRow = (props: Props) => {
},
{
disabled:
isChildUser || !permissions.update_account || paymentMethod.is_default,
isChildUser ||
!permissions.delete_payment_method ||
paymentMethod.is_default,
onClick: onDelete,
title: 'Delete',
tooltip: paymentMethod.is_default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import NumberFormat from 'react-number-format';
import type { NumberFormatProps } from 'react-number-format';
import { makeStyles } from 'tss-react/mui';

import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
import { parseExpiryYear } from 'src/utilities/creditCard';
import { handleAPIErrors } from 'src/utilities/formikErrorUtils';

Expand Down Expand Up @@ -152,9 +153,16 @@ export const AddCreditCardForm = (props: Props) => {
};

const disableInput = isSubmitting || disabled;
const { data: permissions } = usePermissions('account', [
'create_payment_method',
]);

const disableAddButton =
disabled || !values.card_number || !values.cvv || !values.expiry_month;
disabled ||
!values.card_number ||
!values.cvv ||
!values.expiry_month ||
!permissions?.create_payment_method;

return (
<form onSubmit={handleSubmit}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as React from 'react';
import { LinearProgress } from 'src/components/LinearProgress';
import { MAXIMUM_PAYMENT_METHODS } from 'src/constants';
import { getRestrictedResourceText } from 'src/features/Account/utils';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';

import { GooglePayChip } from '../GooglePayChip';
import { PayPalChip } from '../PayPalChip';
Expand Down Expand Up @@ -53,11 +53,12 @@ export const AddPaymentMethodDrawer = (props: Props) => {
PaymentMessage | undefined
>(undefined);
const isChildUser = profile?.user_type === 'child';
const isReadOnly =
useRestrictedGlobalGrantCheck({
globalGrantType: 'account_access',
permittedGrantLevel: 'read_write',
}) || isChildUser;

const { data: permissions } = usePermissions('account', [
'create_payment_method',
]);

const isReadOnly = !permissions?.create_payment_method || isChildUser;

React.useEffect(() => {
if (open) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ vi.mock('@linode/api-v4/lib/account', async () => {
const queryMocks = vi.hoisted(() => ({
useProfile: vi.fn().mockReturnValue({}),
userPermissions: vi.fn(() => ({
data: { update_account: false, make_billing_payment: false },
data: { create_payment_method: false },
})),
}));

Expand Down Expand Up @@ -98,7 +98,7 @@ describe('Payment Info Panel', () => {

it('Opens "Add Payment Method" drawer when "Add Payment Method" is clicked', async () => {
queryMocks.userPermissions.mockReturnValue({
data: { update_account: true, make_billing_payment: true },
data: { create_payment_method: true },
});
const { getByTestId, findByTestId } = renderWithTheme(
<PayPalScriptProvider options={{ clientId: PAYPAL_CLIENT_ID }}>
Expand Down Expand Up @@ -187,7 +187,7 @@ describe('Payment Info Panel', () => {
});

queryMocks.userPermissions.mockReturnValue({
data: { update_account: false, make_billing_payment: false },
data: { create_payment_method: false },
});

const { getByTestId } = renderWithTheme(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ const PaymentInformation = (props: Props) => {

const isChildUser = profile?.user_type === 'child';

const { data: permissions } = usePermissions('account', ['update_account']);
const { data: permissions } = usePermissions('account', [
'create_payment_method',
]);

const isReadOnly = !permissions.update_account || isChildUser;
const isReadOnly = !permissions?.create_payment_method || isChildUser;

const doDelete = () => {
setDeleteLoading(true);
Expand Down