Skip to content

Commit

Permalink
A4A: Restrict destructive operations to members with no capabilities. (
Browse files Browse the repository at this point in the history
…#94100)

* Hide 'Remove site' from team members.

* Hide 'Remove card' action to team members.

* Hide 'Revoke license' from team members.

* Use capabilities instead of role.

* Fix capability name.

* Map team section to capabilities.

* Address PR comments.
  • Loading branch information
jkguidaven authored Sep 3, 2024
1 parent f56df97 commit b5b89c2
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 28 deletions.
4 changes: 4 additions & 0 deletions client/a8c-for-agencies/lib/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
A4A_PAYMENT_METHODS_LINK,
A4A_PAYMENT_METHODS_ADD_LINK,
A4A_MIGRATIONS_LINK,
A4A_TEAM_LINK,
A4A_TEAM_INVITE_LINK,
} from '../components/sidebar-menu/lib/constants';
import type { Agency } from 'calypso/state/a8c-for-agencies/types';

Expand Down Expand Up @@ -79,6 +81,8 @@ const MEMBER_ACCESSIBLE_PATHS: Record< string, string[] > = {
[ A4A_PAYMENT_METHODS_LINK ]: [ 'a4a_jetpack_licensing' ],
[ A4A_PAYMENT_METHODS_ADD_LINK ]: [ 'a4a_jetpack_licensing' ],
[ A4A_MIGRATIONS_LINK ]: [ 'a4a_read_migrations' ],
[ A4A_TEAM_LINK ]: [ 'a4a_read_users' ],
[ A4A_TEAM_INVITE_LINK ]: [ 'a4a_edit_user_invites' ],
};

export const isPathAllowed = ( pathname: string, agency: Agency | null ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
LicenseType,
} from 'calypso/jetpack-cloud/sections/partner-portal/types';
import { addQueryArgs } from 'calypso/lib/url';
import { useDispatch } from 'calypso/state';
import { useDispatch, useSelector } from 'calypso/state';
import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors';
import { A4AStore } from 'calypso/state/a8c-for-agencies/types';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import { errorNotice } from 'calypso/state/notices/actions';
import RevokeLicenseDialog from '../revoke-license-dialog';
Expand Down Expand Up @@ -37,6 +39,10 @@ export default function LicenseDetailsActions( {
const dispatch = useDispatch();
const translate = useTranslate();

const canRevoke = useSelector( ( state: A4AStore ) =>
hasAgencyCapability( state, 'a4a_revoke_licenses' )
);

const [ revokeDialog, setRevokeDialog ] = useState( false );
const isPressableLicense = isPressableHostingProduct( licenseKey );
const pressableManageUrl = 'https://my.pressable.com/agency/auth';
Expand Down Expand Up @@ -107,9 +113,10 @@ export default function LicenseDetailsActions( {
</Button>
) }

{ ( isChildLicense
? licenseState === LicenseState.Attached
: licenseState !== LicenseState.Revoked ) &&
{ canRevoke &&
( isChildLicense
? licenseState === LicenseState.Attached
: licenseState !== LicenseState.Revoked ) &&
licenseType === LicenseType.Partner && (
<Button compact onClick={ openRevokeDialog } scary>
{ translate( 'Revoke' ) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { useCallback, useRef, useState } from 'react';
import PopoverMenu from 'calypso/components/popover-menu';
import PopoverMenuItem from 'calypso/components/popover-menu/item';
import { LicenseRole } from 'calypso/jetpack-cloud/sections/partner-portal/types';
import { useDispatch } from 'calypso/state';
import { useDispatch, useSelector } from 'calypso/state';
import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors';
import { A4AStore } from 'calypso/state/a8c-for-agencies/types';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import RevokeLicenseDialog from '../revoke-license-dialog';

Expand All @@ -19,6 +21,10 @@ export default function LicenseBundleDropDown( { licenseKey, product, bundleSize
const translate = useTranslate();
const dispatch = useDispatch();

const canRevoke = useSelector( ( state: A4AStore ) =>
hasAgencyCapability( state, 'a4a_revoke_licenses' )
);

const [ showRevokeDialog, setShowRevokeDialog ] = useState( false );
const [ showContextMenu, setShowContextMenu ] = useState( false );
const buttonActionRef = useRef< HTMLButtonElement | null >( null );
Expand All @@ -41,6 +47,10 @@ export default function LicenseBundleDropDown( { licenseKey, product, bundleSize
setShowRevokeDialog( false );
}, [ dispatch ] );

if ( ! canRevoke ) {
return null;
}

return (
<>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
LicenseType,
} from 'calypso/jetpack-cloud/sections/partner-portal/types';
import { urlToSlug } from 'calypso/lib/url/http-utils';
import { useDispatch } from 'calypso/state';
import { useDispatch, useSelector } from 'calypso/state';
import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors';
import { A4AStore } from 'calypso/state/a8c-for-agencies/types';
import { recordTracksEvent } from 'calypso/state/analytics/actions';

export default function useLicenseActions(
Expand All @@ -20,6 +22,10 @@ export default function useLicenseActions(
const translate = useTranslate();
const dispatch = useDispatch();

const canRevoke = useSelector( ( state: A4AStore ) =>
hasAgencyCapability( state, 'a4a_revoke_licenses' )
);

return useMemo( () => {
if ( ! siteUrl ) {
return [];
Expand Down Expand Up @@ -75,11 +81,22 @@ export default function useLicenseActions(
onClick: () => handleClickMenuItem( 'calypso_a4a_licenses_hosting_configuration_click' ),
type: 'revoke',
isEnabled:
canRevoke &&
( isChildLicense
? licenseState === LicenseState.Attached
: licenseState !== LicenseState.Revoked ) && licenseType === LicenseType.Partner,
: licenseState !== LicenseState.Revoked ) &&
licenseType === LicenseType.Partner,
className: 'is-destructive',
},
];
}, [ attachedAt, dispatch, isChildLicense, licenseType, revokedAt, siteUrl, translate ] );
}, [
attachedAt,
canRevoke,
dispatch,
isChildLicense,
licenseType,
revokedAt,
siteUrl,
translate,
] );
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default function CreditCardActions( {
const [ isOpen, setIsOpen ] = useState( false );
const dispatch = useDispatch();

const availableActions = cardActions.filter( ( action ) => action.isEnabled );

const showActions = () => {
setIsOpen( true );
dispatch( recordTracksEvent( 'calypso_a4a_payments_card_actions_button_click' ) );
Expand All @@ -31,6 +33,10 @@ export default function CreditCardActions( {
setIsOpen( false );
};

if ( availableActions.length === 0 ) {
return null;
}

return (
<>
<Button
Expand All @@ -50,17 +56,15 @@ export default function CreditCardActions( {
onClose={ closeDropdown }
position="bottom left"
>
{ cardActions
.filter( ( action ) => action.isEnabled )
.map( ( action ) => (
<PopoverMenuItem
className={ clsx( action.className ) }
key={ action.name }
onClick={ action.onClick }
>
{ action.name }
</PopoverMenuItem>
) ) }
{ availableActions.map( ( action ) => (
<PopoverMenuItem
className={ clsx( action.className ) }
key={ action.name }
onClick={ action.onClick }
>
{ action.name }
</PopoverMenuItem>
) ) }
</PopoverMenu>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { PaymentLogo } from '@automattic/wpcom-checkout';
import clsx from 'clsx';
import { useTranslate } from 'i18n-calypso';
import { useContext } from 'react';
import { useDispatch } from 'calypso/state';
import { useDispatch, useSelector } from 'calypso/state';
import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors';
import { A4AStore } from 'calypso/state/a8c-for-agencies/types';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import { PaymentMethodOverviewContext } from '../../context';
import { useDeleteCard } from '../../hooks/use-delete-card';
Expand Down Expand Up @@ -41,6 +43,10 @@ export default function StoredCreditCard( {

const { paging } = useContext( PaymentMethodOverviewContext );

const canRemove = useSelector( ( state: A4AStore ) =>
hasAgencyCapability( state, 'a4a_remove_payment_methods' )
);

// Fetch the stored cards from the cache if they are available.
const {
data: { allStoredCards },
Expand All @@ -64,7 +70,7 @@ export default function StoredCreditCard( {
},
{
name: translate( 'Delete' ),
isEnabled: true,
isEnabled: canRemove,
onClick: () => {
setIsDeleteDialogVisible( true );
dispatch( recordTracksEvent( 'calypso_a4a_payments_card_actions_delete_click' ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { DataViewsState } from 'calypso/a8c-for-agencies/components/items-dashbo
import { A4A_MARKETPLACE_LINK } from 'calypso/a8c-for-agencies/components/sidebar-menu/lib/constants';
import { urlToSlug } from 'calypso/lib/url/http-utils';
import { useDispatch, useSelector } from 'calypso/state';
import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors';
import { A4AStore } from 'calypso/state/a8c-for-agencies/types';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import isAtomicSite from 'calypso/state/selectors/is-site-automated-transfer';
import { isJetpackSite } from 'calypso/state/sites/selectors';
Expand Down Expand Up @@ -43,6 +45,10 @@ export default function useSiteActions( {
const isWPCOMSimpleSite = ! isJetpack && ! isA4AClient;
const isWPCOMSite = isWPCOMSimpleSite || isWPCOMAtomicSite;

const canRemove = useSelector( ( state: A4AStore ) =>
hasAgencyCapability( state, 'a4a_remove_managed_sites' )
);

return useMemo( () => {
if ( ! siteValue ) {
return [];
Expand Down Expand Up @@ -162,17 +168,20 @@ export default function useSiteActions( {
onClick: () => handleClickMenuItem( 'remove_site' ),
icon: 'trash',
className: 'is-error',
isEnabled: true,
isEnabled: canRemove,
},
];
}, [
canRemove,
dispatch,
isDevSite,
isLargeScreen,
isWPCOMSimpleSite,
isWPCOMSite,
onSelect,
setDataViewsState,
setSelectedSiteFeature,
site?.value?.sticker,
site?.value,
siteError,
siteValue,
translate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ export const DateColumn = ( { date }: { date?: string } ): ReactNode => {
export const ActionColumn = ( {
member,
onMenuSelected,
asOwner = true,
canRemove = true,
}: {
member: TeamMember;
onMenuSelected?: ( action: string ) => void;
asOwner?: boolean;
canRemove?: boolean;
} ): ReactNode => {
const translate = useTranslate();

Expand Down Expand Up @@ -121,7 +121,7 @@ export const ActionColumn = ( {
name: 'delete-user',
label: translate( 'Delete user' ),
className: 'is-danger',
isEnabled: asOwner,
isEnabled: canRemove,
},
];

Expand Down
14 changes: 12 additions & 2 deletions client/a8c-for-agencies/sections/team/primary/team-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import LayoutHeader, {
import LayoutTop from 'calypso/a8c-for-agencies/components/layout/top';
import { A4A_TEAM_INVITE_LINK } from 'calypso/a8c-for-agencies/components/sidebar-menu/lib/constants';
import useCancelMemberInviteMutation from 'calypso/a8c-for-agencies/data/team/use-cancel-member-invite';
import { useDispatch } from 'calypso/state';
import { useDispatch, useSelector } from 'calypso/state';
import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors';
import { A4AStore } from 'calypso/state/a8c-for-agencies/types';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import { getCurrentUser } from 'calypso/state/current-user/selectors';
import { errorNotice, successNotice } from 'calypso/state/notices/actions';
import { useMemberList } from '../../hooks/use-member-list';
import { TeamMember } from '../../types';
Expand Down Expand Up @@ -70,6 +73,12 @@ export default function TeamList() {
[ cancelMemberInvite, dispatch, refetch ]
);

const canRemove = useSelector( ( state: A4AStore ) =>
hasAgencyCapability( state, 'a4a_remove_users' )
);

const currentUser = useSelector( getCurrentUser );

const fields = useMemo(
() => [
{
Expand Down Expand Up @@ -111,6 +120,7 @@ export default function TeamList() {
<ActionColumn
member={ item }
onMenuSelected={ ( action ) => handleAction( action, item ) }
canRemove={ canRemove || item.email === currentUser?.email }
/>
);
},
Expand All @@ -119,7 +129,7 @@ export default function TeamList() {
enableSorting: false,
},
],
[ handleAction, isDesktop, translate ]
[ canRemove, currentUser?.email, handleAction, isDesktop, translate ]
);

if ( isPending ) {
Expand Down
11 changes: 11 additions & 0 deletions client/state/a8c-for-agencies/agency/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Required for modular state.
import 'calypso/state/a8c-for-agencies/init';
import { isEnabled } from '@automattic/calypso-config';
import { A4AStore, APIError, Agency } from '../types';

export function getActiveAgency( state: A4AStore ): Agency | null {
Expand Down Expand Up @@ -34,3 +35,13 @@ export function hasAgency( state: A4AStore ): boolean {
export function isAgencyClientUser( state: A4AStore ): boolean {
return state.a8cForAgencies.agencies.isAgencyClientUser;
}

export function hasAgencyCapability( state: A4AStore, capability: string ): boolean {
if ( ! isEnabled( 'a4a-multi-user-support' ) ) {
// This is always true if the feature is not enabled to bypass restrictions.
return true;
}

const agency = getActiveAgency( state );
return agency?.user?.capabilities?.includes( capability ) ?? false;
}

0 comments on commit b5b89c2

Please sign in to comment.