From 6dc152f9d20aeb011eb1b7ec4f17fa910073aee4 Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:45:36 -0800 Subject: [PATCH] fix: [M3-9024] - Mask sensitive data in Managed and Longview (#11476) * Add maskable text to Managed Contacts * Update the docs link URL to a more helpful doc * Make component for reusable copy * Use component in Billing Contact Info and SSH Access Pub Key * Make MaskableText icon styling more flexible; add to Longview command * Add masking to Longview API Key * Clean up styles * Mask IP in Longview * Mask IP Address on SSH Access Managed tab * Fix margin for API Key * Fix spacing for Longview API key * Mask user in Managed and Longview * Added changeset: Visibility of sensitive data in Managed and Longview with Mask Sensitive Data setting enabled --- .../pr-11476-fixed-1736263048231.md | 5 ++ .../components/MaskableText/MaskableText.tsx | 47 ++++++++++++++-- .../MaskableText/MaskableTextArea.tsx | 18 ++++++ .../ContactInfoPanel/ContactInformation.tsx | 8 +-- .../ActiveConnections/ConnectionRow.tsx | 6 +- .../ListeningServices/LongviewServiceRow.tsx | 8 ++- .../DetailTabs/Processes/ProcessesTable.tsx | 10 +++- .../shared/InstallationInstructions.styles.ts | 8 ++- .../shared/InstallationInstructions.tsx | 41 ++++++++++---- .../features/Managed/Contacts/ContactsRow.tsx | 21 +++++-- .../Managed/SSHAccess/LinodePubKey.tsx | 55 +++++++++++++++---- .../Managed/SSHAccess/SSHAccessRow.tsx | 18 +++++- 12 files changed, 194 insertions(+), 51 deletions(-) create mode 100644 packages/manager/.changeset/pr-11476-fixed-1736263048231.md create mode 100644 packages/manager/src/components/MaskableText/MaskableTextArea.tsx diff --git a/packages/manager/.changeset/pr-11476-fixed-1736263048231.md b/packages/manager/.changeset/pr-11476-fixed-1736263048231.md new file mode 100644 index 00000000000..738f0264865 --- /dev/null +++ b/packages/manager/.changeset/pr-11476-fixed-1736263048231.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Visibility of sensitive data in Managed and Longview with Mask Sensitive Data setting enabled ([#11476](https://github.com/linode/manager/pull/11476)) diff --git a/packages/manager/src/components/MaskableText/MaskableText.tsx b/packages/manager/src/components/MaskableText/MaskableText.tsx index 2fed57bf4d9..dbeaedb1988 100644 --- a/packages/manager/src/components/MaskableText/MaskableText.tsx +++ b/packages/manager/src/components/MaskableText/MaskableText.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import { usePreferences } from 'src/queries/profile/preferences'; import { createMaskedText } from 'src/utilities/createMaskedText'; +import type { SxProps, Theme } from '@mui/material'; + export type MaskableTextLength = 'ipv4' | 'ipv6' | 'plaintext'; export interface MaskableTextProps { @@ -12,14 +14,28 @@ export interface MaskableTextProps { * (Optional) original JSX element to render if the text is not masked. */ children?: JSX.Element | JSX.Element[]; + /** + * Optionally specifies the position of the VisibilityTooltip icon either before or after the text. + * @default end + */ + iconPosition?: 'end' | 'start'; /** * If true, displays a VisibilityTooltip icon to toggle the masked and unmasked text. + * @default false */ isToggleable?: boolean; /** * Optionally specifies the length of the masked text to depending on data type (e.g. 'ipv4', 'ipv6', 'plaintext'); if not provided, will use a default length. */ length?: MaskableTextLength; + /** + * Optional styling for the masked and unmasked Typography + */ + sxTypography?: SxProps; + /** + * Optional styling for VisibilityTooltip icon + */ + sxVisibilityTooltip?: SxProps; /** * The original, maskable text; if the text is not masked, render this text or the styled text via children. */ @@ -27,7 +43,15 @@ export interface MaskableTextProps { } export const MaskableText = (props: MaskableTextProps) => { - const { children, isToggleable = false, length, text } = props; + const { + children, + iconPosition = 'end', + isToggleable = false, + length, + sxTypography, + sxVisibilityTooltip, + text, + } = props; const { data: maskedPreferenceSetting } = usePreferences( (preferences) => preferences?.maskSensitiveData @@ -35,7 +59,11 @@ export const MaskableText = (props: MaskableTextProps) => { const [isMasked, setIsMasked] = React.useState(maskedPreferenceSetting); - const unmaskedText = children ? children : {text}; + const unmaskedText = children ? ( + children + ) : ( + {text} + ); // Return early based on the preference setting and the original text. @@ -54,14 +82,25 @@ export const MaskableText = (props: MaskableTextProps) => { flexDirection="row" justifyContent="flex-start" > + {iconPosition === 'start' && isToggleable && ( + setIsMasked(!isMasked)} + isVisible={!isMasked} + /> + )} {isMasked ? ( - + {createMaskedText(text, length)} ) : ( unmaskedText )} - {isToggleable && ( + {iconPosition === 'end' && isToggleable && ( setIsMasked(!isMasked)} isVisible={!isMasked} diff --git a/packages/manager/src/components/MaskableText/MaskableTextArea.tsx b/packages/manager/src/components/MaskableText/MaskableTextArea.tsx new file mode 100644 index 00000000000..abbeb85697a --- /dev/null +++ b/packages/manager/src/components/MaskableText/MaskableTextArea.tsx @@ -0,0 +1,18 @@ +import { Typography } from '@linode/ui'; +import React from 'react'; + +import { Link } from '../Link'; + +/** + * This copy is intended to display where a larger area of data is masked. + * Example: Billing Contact info, rather than masking many individual fields + */ +export const MaskableTextAreaCopy = () => { + return ( + + This data is sensitive and hidden for privacy. To unmask all sensitive + data by default, go to{' '} + profile settings. + + ); +}; diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx index 8fd5b6c4157..2c85cd0b195 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useState } from 'react'; import { useHistory, useRouteMatch } from 'react-router-dom'; -import { Link } from 'src/components/Link'; +import { MaskableTextAreaCopy } from 'src/components/MaskableText/MaskableTextArea'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { EDIT_BILLING_CONTACT } from 'src/features/Billing/constants'; import { StyledAutorenewIcon } from 'src/features/TopMenu/NotificationMenu/NotificationMenu'; @@ -198,11 +198,7 @@ export const ContactInformation = React.memo((props: Props) => { {maskSensitiveDataPreference && isContactInfoMasked ? ( - - This data is sensitive and hidden for privacy. To unmask all - sensitive data by default, go to{' '} - profile settings. - + ) : ( {(firstName || diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx index b4e99046a7e..58bba78afee 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ActiveConnections/ConnectionRow.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { LongviewPort } from 'src/features/Longview/request.types'; + +import type { LongviewPort } from 'src/features/Longview/request.types'; interface Props { connection: LongviewPort; @@ -17,7 +19,7 @@ export const ConnectionRow = (props: Props) => { {connection.name} - {connection.user} + {connection.count} diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx index 001531b60f3..5aaf46b81ad 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/ListeningServices/LongviewServiceRow.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { LongviewService } from 'src/features/Longview/request.types'; + +import type { LongviewService } from 'src/features/Longview/request.types'; interface Props { service: LongviewService; @@ -17,7 +19,7 @@ export const LongviewServiceRow = (props: Props) => { {service.name} - {service.user} + {service.type} @@ -26,7 +28,7 @@ export const LongviewServiceRow = (props: Props) => { {service.port} - {service.ip} + ); diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx index 4089557afda..6862e19c93d 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesTable.tsx @@ -1,6 +1,6 @@ -import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; import OrderBy from 'src/components/OrderBy'; import { TableBody } from 'src/components/TableBody'; import { TableCell } from 'src/components/TableCell'; @@ -15,7 +15,9 @@ import { useWindowDimensions } from 'src/hooks/useWindowDimensions'; import { readableBytes } from 'src/utilities/unitConversions'; import { StyledDiv, StyledTable } from './ProcessesTable.styles'; -import { Process } from './types'; + +import type { Process } from './types'; +import type { APIError } from '@linode/api-v4/lib/types'; export interface ProcessesTableProps { error?: string; @@ -184,7 +186,9 @@ export const ProcessesTableRow = React.memo((props: ProcessTableRowProps) => { {name} - {user} + + + {Math.round(maxCount)} diff --git a/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts b/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts index 266706a54e5..4827104cbd9 100644 --- a/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts +++ b/packages/manager/src/features/Longview/shared/InstallationInstructions.styles.ts @@ -4,6 +4,12 @@ import Grid from '@mui/material/Unstable_Grid2'; export const StyledInstructionGrid = styled(Grid, { label: 'StyledInstructionGrid', })(({ theme }) => ({ + boxSizing: 'border-box', + columnGap: 1, + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + margin: '0', [theme.breakpoints.up('sm')]: { '&:not(:first-of-type)': { '&:before': { @@ -19,8 +25,6 @@ export const StyledInstructionGrid = styled(Grid, { width: 'auto', }, width: '100%', - boxSizing: 'border-box', - margin: '0', })); export const StyledContainerGrid = styled(Grid, { diff --git a/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx b/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx index 0d643784e03..316c1fb83f2 100644 --- a/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx +++ b/packages/manager/src/features/Longview/shared/InstallationInstructions.tsx @@ -1,9 +1,11 @@ -import { Box, Typography } from '@linode/ui'; +import { Typography } from '@linode/ui'; +import { useTheme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { Link } from 'src/components/Link'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { StyledContainerGrid, @@ -17,6 +19,7 @@ interface Props { export const InstallationInstructions = React.memo((props: Props) => { const command = `curl -s https://lv.linode.com/${props.installationKey} | sudo bash`; + const theme = useTheme(); return ( @@ -41,9 +44,22 @@ export const InstallationInstructions = React.memo((props: Props) => { paddingTop: 0, }} > -
-              {command}
-            
+ +
+                {command}
+              
+
@@ -71,15 +87,16 @@ export const InstallationInstructions = React.memo((props: Props) => {
- - API Key:{' '} - ({ color: theme.color.grey1 })} - > - {props.APIKey} - + + API Key: + diff --git a/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx b/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx index 2ae785f27ef..78ee76774e4 100644 --- a/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx +++ b/packages/manager/src/features/Managed/Contacts/ContactsRow.tsx @@ -5,6 +5,7 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import ActionMenu from './ContactsActionMenu'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; import type { ManagedContact } from '@linode/api-v4/lib/managed'; @@ -19,14 +20,24 @@ export const ContactsRow = (props: ContactsRowProps) => { return ( - {contact.name} + + + - {contact.group} + + + - {contact.email} + + + - {contact.phone.primary} - {contact.phone.secondary} + + + + + + { const { data, error, isLoading } = useManagedSSHKey(); + const { data: preferences } = usePreferences(); const [copied, setCopied] = React.useState(false); + const [isSSHKeyMasked, setIsSSHKeyMasked] = React.useState( + preferences?.maskSensitiveData + ); const timeout = React.useRef(); + const matchesSmDownBreakpoint = useMediaQuery((theme: Theme) => + theme.breakpoints.down('sm') + ); React.useEffect(() => { if (copied) { @@ -71,27 +86,45 @@ const LinodePubKey = () => { - + Linode Public Key - + You must install our public SSH key on all managed Linodes so we can access them and diagnose issues. - {/* Hide the SSH key on x-small viewports */} - + - {data?.ssh_key || ''} + {preferences?.maskSensitiveData && isSSHKeyMasked ? ( + + ) : ( + data?.ssh_key || '' + )} {/* See NOTE A. If that CSS is removed, we can use the following instead: */} {/* pubKey.slice(0, 160)} . . . */} - + + + {preferences?.maskSensitiveData && ( + + )} + diff --git a/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx b/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx index 1bfc78f00f7..f4cbc97b773 100644 --- a/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/SSHAccessRow.tsx @@ -1,12 +1,14 @@ -import { ManagedLinodeSetting } from '@linode/api-v4/lib/managed'; import * as React from 'react'; import { Hidden } from 'src/components/Hidden'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import ActionMenu from './SSHAccessActionMenu'; +import type { ManagedLinodeSetting } from '@linode/api-v4/lib/managed'; + interface SSHAccessRowProps { linodeSetting: ManagedLinodeSetting; openDrawer: (linodeId: number) => void; @@ -28,9 +30,19 @@ export const SSHAccessRow = (props: SSHAccessRowProps) => { {isAccessEnabled ? 'Enabled' : 'Disabled'} - {linodeSetting.ssh.user} + + + - {linodeSetting.ssh.ip === 'any' ? 'Any' : linodeSetting.ssh.ip} + {linodeSetting.ssh.ip === 'any' ? ( + 'Any' + ) : ( + + )} {linodeSetting.ssh.port}