Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [M3-9024] - Mask sensitive data in Managed and Longview #11476

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
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
Loading