Skip to content

Commit cdcf176

Browse files
change: [UIE-8836] - IAM RBAC permissions for Billing Activity (linode#12660)
* BillingActivityPanel * Added changeset: IAM RBAC permissions for Billing Activity * list_invoice_items * cleanup * moar cleanuo * Added changeset: Implemented `disabled` parameters for payments & invoices queries * update changeset * feedback @coliu-akamai
1 parent faad3d0 commit cdcf176

File tree

6 files changed

+98
-19
lines changed

6 files changed

+98
-19
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Changed
3+
---
4+
5+
IAM RBAC permissions for Billing Activity ([#12660](https://github.com/linode/manager/pull/12660))

packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
useProfile,
77
useRegionsQuery,
88
} from '@linode/queries';
9-
import { Autocomplete, Typography } from '@linode/ui';
9+
import { Autocomplete, Notice, Typography, WarningIcon } from '@linode/ui';
1010
import { getAll, useSet } from '@linode/utilities';
1111
import Grid from '@mui/material/Grid';
1212
import Paper from '@mui/material/Paper';
@@ -37,6 +37,7 @@ import {
3737
printInvoice,
3838
printPayment,
3939
} from 'src/features/Billing/PdfGenerator/PdfGenerator';
40+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
4041
import { useFlags } from 'src/hooks/useFlags';
4142
import { useOrderV2 } from 'src/hooks/useOrderV2';
4243
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
@@ -199,6 +200,16 @@ export const BillingActivityPanel = React.memo((props: Props) => {
199200
preferenceKey: 'billing-activity-order',
200201
});
201202

203+
const { data: permissions } = usePermissions('account', [
204+
'list_billing_payments',
205+
'list_billing_invoices',
206+
'list_invoice_items',
207+
]);
208+
209+
const canViewInvoices = permissions.list_billing_invoices;
210+
const canViewPayments = permissions.list_billing_payments;
211+
const canViewInvoiceDetails = permissions.list_invoice_items;
212+
202213
const isAkamaiCustomer = account?.billing_source === 'akamai';
203214
const { classes } = useStyles();
204215
const flags = useFlags();
@@ -218,13 +229,13 @@ export const BillingActivityPanel = React.memo((props: Props) => {
218229
data: payments,
219230
error: accountPaymentsError,
220231
isLoading: accountPaymentsLoading,
221-
} = useAllAccountPayments({}, filter);
232+
} = useAllAccountPayments({}, filter, canViewPayments);
222233

223234
const {
224235
data: invoices,
225236
error: accountInvoicesError,
226237
isLoading: accountInvoicesLoading,
227-
} = useAllAccountInvoices({}, filter);
238+
} = useAllAccountInvoices({}, filter, canViewInvoices);
228239

229240
const downloadInvoicePDF = React.useCallback(
230241
(invoiceId: number) => {
@@ -356,7 +367,19 @@ export const BillingActivityPanel = React.memo((props: Props) => {
356367
return (
357368
<TableRowEmpty
358369
colSpan={NUM_COLS}
359-
message="No Billing & Payment History found."
370+
message={
371+
canViewInvoices && canViewPayments ? (
372+
'No Billing & Payment History found.'
373+
) : (
374+
<>
375+
<WarningIcon
376+
style={{ position: 'relative', top: 4, marginRight: 2 }}
377+
width={16}
378+
/>{' '}
379+
You do not have permission to view billing or payment history.
380+
</>
381+
)
382+
}
360383
/>
361384
);
362385
}
@@ -365,6 +388,7 @@ export const BillingActivityPanel = React.memo((props: Props) => {
365388
const lastItem = idx === orderedPaginatedData.length - 1;
366389
return (
367390
<ActivityFeedItem
391+
canViewInvoiceDetails={canViewInvoiceDetails}
368392
downloadPDF={
369393
thisItem.type === 'invoice'
370394
? downloadInvoicePDF
@@ -428,6 +452,16 @@ export const BillingActivityPanel = React.memo((props: Props) => {
428452
<Autocomplete
429453
className={classes.transactionType}
430454
disableClearable
455+
disabled={!canViewInvoices && !canViewPayments}
456+
filterOptions={(options) => {
457+
if (!canViewInvoices) {
458+
return options.filter((option) => option.value !== 'invoice');
459+
}
460+
if (!canViewPayments) {
461+
return options.filter((option) => option.value !== 'payment');
462+
}
463+
return options;
464+
}}
431465
label="Transaction Types"
432466
noMarginTop
433467
onChange={(_, item) => {
@@ -457,6 +491,16 @@ export const BillingActivityPanel = React.memo((props: Props) => {
457491
/>
458492
</div>
459493
</StyledBillingAndPaymentHistoryHeader>
494+
{(canViewInvoices && !canViewPayments) ||
495+
(!canViewInvoices && canViewPayments) ? (
496+
<Notice
497+
spacingBottom={20}
498+
text={`You do not have permission to view ${
499+
canViewInvoices ? 'payments' : 'invoices'
500+
} history.`}
501+
variant="error"
502+
/>
503+
) : null}
460504
<Table aria-label="List of Invoices and Payments" sx={{ border: 0 }}>
461505
<TableHead>
462506
<TableRow>
@@ -473,8 +517,9 @@ export const BillingActivityPanel = React.memo((props: Props) => {
473517
>
474518
Amount
475519
</TableSortCell>
476-
477-
<TableCell className={classes.pdfDownloadColumn} />
520+
{canViewInvoiceDetails && (
521+
<TableCell className={classes.pdfDownloadColumn} />
522+
)}
478523
</TableRow>
479524
</TableHead>
480525
<TableBody>{renderTableContent()}</TableBody>
@@ -503,6 +548,7 @@ const StyledBillingAndPaymentHistoryHeader = styled('div', {
503548
// <ActivityFeedItem />
504549
// =============================================================================
505550
interface ActivityFeedItemProps extends ActivityFeedItem {
551+
canViewInvoiceDetails: boolean;
506552
downloadPDF: (id: number) => void;
507553
hasError: boolean;
508554
isLoading: boolean;
@@ -515,6 +561,7 @@ export const ActivityFeedItem = React.memo((props: ActivityFeedItemProps) => {
515561
const { iamRbacPrimaryNavChanges } = useFlags();
516562

517563
const {
564+
canViewInvoiceDetails,
518565
date,
519566
downloadPDF,
520567
hasError,
@@ -544,7 +591,7 @@ export const ActivityFeedItem = React.memo((props: ActivityFeedItemProps) => {
544591
return (
545592
<TableRow data-testid={`${type}-${id}`} sx={sxRow}>
546593
<TableCell>
547-
{type === 'invoice' ? (
594+
{type === 'invoice' && canViewInvoiceDetails ? (
548595
<Link
549596
to={
550597
iamRbacPrimaryNavChanges
@@ -564,14 +611,16 @@ export const ActivityFeedItem = React.memo((props: ActivityFeedItemProps) => {
564611
<TableCell className={classes.totalColumn}>
565612
<Currency quantity={total} wrapInParentheses={total < 0} />
566613
</TableCell>
567-
<TableCell className={classes.pdfDownloadColumn}>
568-
<InlineMenuAction
569-
actionText={action.title}
570-
className={action.className}
571-
loading={isLoading}
572-
onClick={action.onClick}
573-
/>
574-
</TableCell>
614+
{canViewInvoiceDetails && (
615+
<TableCell className={classes.pdfDownloadColumn}>
616+
<InlineMenuAction
617+
actionText={action.title}
618+
className={action.className}
619+
loading={isLoading}
620+
onClick={action.onClick}
621+
/>
622+
</TableCell>
623+
)}
575624
</TableRow>
576625
);
577626
});

packages/manager/src/features/Billing/InvoiceDetail/InvoiceDetail.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DownloadCSV } from 'src/components/DownloadCSV/DownloadCSV';
2222
import { LandingHeader } from 'src/components/LandingHeader';
2323
import { Link } from 'src/components/Link';
2424
import { printInvoice } from 'src/features/Billing/PdfGenerator/PdfGenerator';
25+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
2526
import { useFlags } from 'src/hooks/useFlags';
2627
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
2728

@@ -41,6 +42,9 @@ export const InvoiceDetail = () => {
4142
: '/account/billing/invoices/$invoiceId',
4243
});
4344
const theme = useTheme();
45+
const { data: permissions } = usePermissions('account', [
46+
'list_invoice_items',
47+
]);
4448

4549
const csvRef = React.useRef<any>(undefined);
4650

@@ -59,6 +63,10 @@ export const InvoiceDetail = () => {
5963
const shouldShowRegion = invoiceCreatedAfterDCPricingLaunch(invoice?.date);
6064

6165
const requestData = () => {
66+
if (!permissions.list_invoice_items) {
67+
return;
68+
}
69+
6270
setLoading(true);
6371

6472
const getAllInvoiceItems = getAll<InvoiceItem>((params, filter) =>
@@ -86,6 +94,14 @@ export const InvoiceDetail = () => {
8694
requestData();
8795
}, []);
8896

97+
if (!permissions.list_invoice_items) {
98+
return (
99+
<Notice variant="error">
100+
You do not have permission to view invoice details.
101+
</Notice>
102+
);
103+
}
104+
89105
const printInvoicePDF = async (
90106
account: Account,
91107
invoice: Invoice,

packages/manager/src/features/CloudPulse/Utils/FilterBuilder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -634,11 +634,11 @@ export const getFilters = (
634634
return FILTER_CONFIG.get(dashboard.id)?.filters.filter((config) =>
635635
isServiceAnalyticsIntegration
636636
? config.configuration.neededInViews.includes(
637-
CloudPulseAvailableViews.service
638-
)
637+
CloudPulseAvailableViews.service
638+
)
639639
: config.configuration.neededInViews.includes(
640-
CloudPulseAvailableViews.central
641-
)
640+
CloudPulseAvailableViews.central
641+
)
642642
);
643643
};
644644

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/queries": Added
3+
---
4+
5+
Implemented `enabled` parameters for payments & invoices queries ([#12660](https://github.com/linode/manager/pull/12660))

packages/queries/src/account/billing.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,26 @@ import type { APIError, Filter, Params } from '@linode/api-v4/lib/types';
1010
export const useAllAccountInvoices = (
1111
params: Params = {},
1212
filter: Filter = {},
13+
enabled: boolean = true,
1314
) => {
1415
return useQuery<Invoice[], APIError[]>({
1516
...accountQueries.invoices(params, filter),
1617
...queryPresets.oneTimeFetch,
1718
placeholderData: keepPreviousData,
19+
enabled,
1820
});
1921
};
2022

2123
export const useAllAccountPayments = (
2224
params: Params = {},
2325
filter: Filter = {},
26+
enabled: boolean = true,
2427
) => {
2528
return useQuery<Payment[], APIError[]>({
2629
...accountQueries.payments(params, filter),
2730
...queryPresets.oneTimeFetch,
2831
placeholderData: keepPreviousData,
32+
enabled,
2933
});
3034
};
3135

0 commit comments

Comments
 (0)