Skip to content

Commit

Permalink
fix: [M3-9024] - Mask sensitive data in Managed and Longview (#11476)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mjac0bs authored Jan 7, 2025
1 parent 26e3b83 commit 6dc152f
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 51 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11476-fixed-1736263048231.md
Original file line number Diff line number Diff line change
@@ -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))
47 changes: 43 additions & 4 deletions packages/manager/src/components/MaskableText/MaskableText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,65 @@ 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 {
/**
* (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<Theme>;
/**
* Optional styling for VisibilityTooltip icon
*/
sxVisibilityTooltip?: SxProps<Theme>;
/**
* The original, maskable text; if the text is not masked, render this text or the styled text via children.
*/
text: string | undefined;
}

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
);

const [isMasked, setIsMasked] = React.useState(maskedPreferenceSetting);

const unmaskedText = children ? children : <Typography>{text}</Typography>;
const unmaskedText = children ? (
children
) : (
<Typography sx={sxTypography}>{text}</Typography>
);

// Return early based on the preference setting and the original text.

Expand All @@ -54,14 +82,25 @@ export const MaskableText = (props: MaskableTextProps) => {
flexDirection="row"
justifyContent="flex-start"
>
{iconPosition === 'start' && isToggleable && (
<VisibilityTooltip
sx={{
marginLeft: 0,
marginRight: '8px',
...sxVisibilityTooltip,
}}
handleClick={() => setIsMasked(!isMasked)}
isVisible={!isMasked}
/>
)}
{isMasked ? (
<Typography sx={{ overflowWrap: 'anywhere' }}>
<Typography sx={{ overflowWrap: 'anywhere', ...sxTypography }}>
{createMaskedText(text, length)}
</Typography>
) : (
unmaskedText
)}
{isToggleable && (
{iconPosition === 'end' && isToggleable && (
<VisibilityTooltip
handleClick={() => setIsMasked(!isMasked)}
isVisible={!isMasked}
Expand Down
18 changes: 18 additions & 0 deletions packages/manager/src/components/MaskableText/MaskableTextArea.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Typography>
This data is sensitive and hidden for privacy. To unmask all sensitive
data by default, go to{' '}
<Link to="/profile/settings">profile settings</Link>.
</Typography>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -198,11 +198,7 @@ export const ContactInformation = React.memo((props: Props) => {
</Box>
</BillingBox>
{maskSensitiveDataPreference && isContactInfoMasked ? (
<Typography>
This data is sensitive and hidden for privacy. To unmask all
sensitive data by default, go to{' '}
<Link to="/profile/settings">profile settings</Link>.
</Typography>
<MaskableTextAreaCopy />
) : (
<Grid container spacing={2}>
{(firstName ||
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,7 +19,7 @@ export const ConnectionRow = (props: Props) => {
{connection.name}
</TableCell>
<TableCell data-qa-active-connection-user parentColumn="User">
{connection.user}
<MaskableText isToggleable text={connection.user} />
</TableCell>
<TableCell data-qa-active-connection-count parentColumn="Count">
{connection.count}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,7 +19,7 @@ export const LongviewServiceRow = (props: Props) => {
{service.name}
</TableCell>
<TableCell data-qa-service-user parentColumn="User">
{service.user}
<MaskableText isToggleable text={service.user} />
</TableCell>
<TableCell data-qa-service-protocol parentColumn="Protocol">
{service.type}
Expand All @@ -26,7 +28,7 @@ export const LongviewServiceRow = (props: Props) => {
{service.port}
</TableCell>
<TableCell data-qa-service-ip parentColumn="IP">
{service.ip}
<MaskableText isToggleable text={service.ip} />
</TableCell>
</TableRow>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -184,7 +186,9 @@ export const ProcessesTableRow = React.memo((props: ProcessTableRowProps) => {
<TableCell data-testid={`name-${name}`}>
<StyledDiv>{name}</StyledDiv>
</TableCell>
<TableCell data-testid={`user-${user}`}>{user}</TableCell>
<TableCell data-testid={`user-${user}`}>
<MaskableText isToggleable text={user} />
</TableCell>
<TableCell data-testid={`max-count-${Math.round(maxCount)}`}>
{Math.round(maxCount)}
</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand All @@ -19,8 +25,6 @@ export const StyledInstructionGrid = styled(Grid, {
width: 'auto',
},
width: '100%',
boxSizing: 'border-box',
margin: '0',
}));

export const StyledContainerGrid = styled(Grid, {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<Grid container spacing={2}>
Expand All @@ -41,9 +44,22 @@ export const InstallationInstructions = React.memo((props: Props) => {
paddingTop: 0,
}}
>
<pre>
<code>{command}</code>
</pre>
<MaskableText
sxVisibilityTooltip={{
'& svg': {
height: 'auto',
width: '20px',
},
marginRight: '24px',
}}
iconPosition="start"
isToggleable
text={command}
>
<pre>
<code>{command}</code>
</pre>
</MaskableText>
</Grid>
</StyledContainerGrid>
</Grid>
Expand Down Expand Up @@ -71,15 +87,16 @@ export const InstallationInstructions = React.memo((props: Props) => {
</Typography>
</StyledInstructionGrid>
<StyledInstructionGrid>
<Typography data-testid="api-key">
API Key:{' '}
<Box
component="span"
sx={(theme) => ({ color: theme.color.grey1 })}
>
{props.APIKey}
</Box>
<Typography data-testid="api-key" sx={{ marginRight: 0.5 }}>
API Key:
</Typography>
<MaskableText
iconPosition="start"
isToggleable
sxTypography={{ color: theme.color.grey1 }}
sxVisibilityTooltip={{ marginLeft: 1 }}
text={props.APIKey}
/>
</StyledInstructionGrid>
</Grid>
</Grid>
Expand Down
21 changes: 16 additions & 5 deletions packages/manager/src/features/Managed/Contacts/ContactsRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -19,14 +20,24 @@ export const ContactsRow = (props: ContactsRowProps) => {

return (
<TableRow key={contact.id}>
<TableCell>{contact.name}</TableCell>
<TableCell>
<MaskableText text={contact.name} isToggleable />
</TableCell>
<Hidden mdDown>
<TableCell>{contact.group}</TableCell>
<TableCell>
<MaskableText text={contact.group ?? ''} isToggleable />
</TableCell>
</Hidden>
<TableCell>{contact.email}</TableCell>
<TableCell>
<MaskableText text={contact.email} isToggleable />
</TableCell>
<Hidden xsDown>
<TableCell>{contact.phone.primary}</TableCell>
<TableCell>{contact.phone.secondary}</TableCell>
<TableCell>
<MaskableText text={contact.phone.primary ?? ''} isToggleable />
</TableCell>
<TableCell>
<MaskableText text={contact.phone.secondary ?? ''} isToggleable />
</TableCell>
</Hidden>
<TableCell actionCell>
<ActionMenu
Expand Down
Loading

0 comments on commit 6dc152f

Please sign in to comment.