diff --git a/lib/services/useracl.go b/lib/services/useracl.go index d1511651f8adc..4a6c39b554ee5 100644 --- a/lib/services/useracl.go +++ b/lib/services/useracl.go @@ -118,6 +118,8 @@ type UserACL struct { ReviewRequests bool `json:"reviewRequests"` // Contact defines the ability to manage contacts Contact ResourceAccess `json:"contact"` + // FileTransferAccess defines the ability to perform remote file operations via SCP or SFTP + FileTransferAccess bool `json:"fileTransferAccess"` } func hasAccess(roleSet RoleSet, ctx *Context, kind string, verbs ...string) bool { @@ -210,6 +212,7 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des crownJewelAccess := newAccess(userRoles, ctx, types.KindCrownJewel) userTasksAccess := newAccess(userRoles, ctx, types.KindUserTask) reviewRequests := userRoles.MaybeCanReviewRequests() + fileTransferAccess := userRoles.CanCopyFiles() var auditQuery ResourceAccess var securityReports ResourceAccess @@ -262,5 +265,6 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des CrownJewel: crownJewelAccess, AccessGraphSettings: accessGraphSettings, Contact: contact, + FileTransferAccess: fileTransferAccess, } } diff --git a/web/packages/shared/components/FileTransfer/FileTransferActionBar.test.tsx b/web/packages/shared/components/FileTransfer/FileTransferActionBar.test.tsx new file mode 100644 index 0000000000000..9f948abeda683 --- /dev/null +++ b/web/packages/shared/components/FileTransfer/FileTransferActionBar.test.tsx @@ -0,0 +1,44 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { render, screen } from 'design/utils/testing'; + +import { FileTransferActionBar } from './FileTransferActionBar'; +import { FileTransferContextProvider } from './FileTransferContextProvider'; + +test('file transfer bar is enabled by default', async () => { + render( + + + + ); + + expect(screen.getByTitle('Download files')).toBeEnabled(); + expect(screen.getByTitle('Upload files')).toBeEnabled(); +}); + +test('file transfer is disable if no access', async () => { + render( + + + + ); + + expect(screen.getByTitle('Download files')).toBeDisabled(); + expect(screen.getByTitle('Upload files')).toBeDisabled(); +}); diff --git a/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx b/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx index de894ca2e0d89..720f42b226f0a 100644 --- a/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx +++ b/web/packages/shared/components/FileTransfer/FileTransferActionBar.tsx @@ -17,40 +17,59 @@ */ import React from 'react'; -import { Flex, ButtonIcon } from 'design'; +import { Flex, ButtonIcon, Text } from 'design'; import * as Icons from 'design/Icon'; +import { HoverTooltip } from '../ToolTip'; + import { useFileTransferContext } from './FileTransferContextProvider'; type FileTransferActionBarProps = { isConnected: boolean; + // if any role has `options.ssh_file_copy: true` without any other role having `false` (false overrides). + hasAccess: boolean; }; export function FileTransferActionBar({ isConnected, + hasAccess, }: FileTransferActionBarProps) { const fileTransferContext = useFileTransferContext(); const areFileTransferButtonsDisabled = - fileTransferContext.openedDialog || !isConnected; + fileTransferContext.openedDialog || !isConnected || !hasAccess; return ( - - - - + You are missing the{' '} + + ssh_file_copy + {' '} + role option. + + ) : null + } > - - + + + + + + + ); } diff --git a/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.test.tsx b/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.test.tsx new file mode 100644 index 0000000000000..0003326ef754f --- /dev/null +++ b/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.test.tsx @@ -0,0 +1,68 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { render, screen } from 'design/utils/testing'; +import { MemoryRouter } from 'react-router'; +import 'jest-canvas-mock'; + +import { allAccessAcl } from 'teleport/mocks/contexts'; + +import ConsoleContext from '../consoleContext'; +import ConsoleContextProvider from '../consoleContextProvider'; + +import DocumentSsh from '.'; + +test('file transfer buttons are disabled if user does not have access', async () => { + const ctx = new ConsoleContext(); + ctx.storeUser.setState({ + acl: { ...allAccessAcl, fileTransferAccess: false }, + }); + render(); + expect(screen.getByTitle('Download files')).toBeDisabled(); +}); + +test('file transfer buttons are enabled if user has access', async () => { + const ctx = new ConsoleContext(); + ctx.storeUser.setState({ + acl: allAccessAcl, + }); + render(); + expect(screen.getByTitle('Download files')).toBeEnabled(); +}); + +function Component({ ctx }: { ctx: ConsoleContext }) { + return ( + + + + + + ); +} diff --git a/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx b/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx index 01c64aea242ed..7bbd0dfa16b0c 100644 --- a/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx +++ b/web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx @@ -36,6 +36,8 @@ import useWebAuthn from 'teleport/lib/useWebAuthn'; import Document from '../Document'; +import { useConsoleContext } from '../consoleContextProvider'; + import { Terminal, TerminalRef } from './Terminal'; import useSshSession from './useSshSession'; import { useFileTransfer } from './useFileTransfer'; @@ -49,6 +51,8 @@ export default function DocumentSshWrapper(props: PropTypes) { } function DocumentSsh({ doc, visible }: PropTypes) { + const ctx = useConsoleContext(); + const hasFileTransferAccess = ctx.storeUser.hasFileTransferAccess(); const terminalRef = useRef(); const { tty, status, closeDocument, session } = useSshSession(doc); const [showSearch, setShowSearch] = useState(false); @@ -130,7 +134,10 @@ function DocumentSsh({ doc, visible }: PropTypes) { return ( - + {status === 'loading' && ( diff --git a/web/packages/teleport/src/mocks/contexts.ts b/web/packages/teleport/src/mocks/contexts.ts index 9540513d3625e..45586517515aa 100644 --- a/web/packages/teleport/src/mocks/contexts.ts +++ b/web/packages/teleport/src/mocks/contexts.ts @@ -60,6 +60,7 @@ export const allAccessAcl: Acl = { desktopSessionRecordingEnabled: true, directorySharingEnabled: true, reviewRequests: true, + fileTransferAccess: true, license: fullAccess, download: fullAccess, plugins: fullAccess, diff --git a/web/packages/teleport/src/services/user/makeAcl.ts b/web/packages/teleport/src/services/user/makeAcl.ts index 6fba5fe7b5a9f..70d0410ba2db4 100644 --- a/web/packages/teleport/src/services/user/makeAcl.ts +++ b/web/packages/teleport/src/services/user/makeAcl.ts @@ -40,6 +40,11 @@ export function makeAcl(json): Acl { const db = json.db || defaultAccess; const desktops = json.desktops || defaultAccess; const reviewRequests = json.reviewRequests ?? false; + // TODO (avatus) change default to false in v19. We do not want someone + // who _can_ access file transfers to be denied access because an older cluster + // doesn't return the valid permission. If they don't have access, the action will + // still fail with an error, so this is merely a UX improvment. + const fileTransferAccess = json.fileTransferAccess ?? true; // use nullish coalescing to prevent default from overriding a strictly false value const connectionDiagnostic = json.connectionDiagnostic || defaultAccess; // Defaults to true, see RFD 0049 // https://github.com/gravitational/teleport/blob/master/rfd/0049-desktop-clipboard.md#security @@ -112,6 +117,7 @@ export function makeAcl(json): Acl { bots, accessMonitoringRule, contacts, + fileTransferAccess, }; } diff --git a/web/packages/teleport/src/services/user/types.ts b/web/packages/teleport/src/services/user/types.ts index be84afc059a23..4797af9254973 100644 --- a/web/packages/teleport/src/services/user/types.ts +++ b/web/packages/teleport/src/services/user/types.ts @@ -106,6 +106,7 @@ export interface Acl { bots: Access; accessMonitoringRule: Access; contacts: Access; + fileTransferAccess: boolean; } // AllTraits represent all the traits defined for a user. diff --git a/web/packages/teleport/src/services/user/user.test.ts b/web/packages/teleport/src/services/user/user.test.ts index dc95b4e322a18..39153be22c668 100644 --- a/web/packages/teleport/src/services/user/user.test.ts +++ b/web/packages/teleport/src/services/user/user.test.ts @@ -281,6 +281,7 @@ test('undefined values in context response gives proper default values', async ( clipboardSharingEnabled: true, desktopSessionRecordingEnabled: true, directorySharingEnabled: true, + fileTransferAccess: true, }; expect(response).toEqual({ diff --git a/web/packages/teleport/src/stores/storeUserContext.ts b/web/packages/teleport/src/stores/storeUserContext.ts index c80431558f2dc..c6fdb6839019e 100644 --- a/web/packages/teleport/src/stores/storeUserContext.ts +++ b/web/packages/teleport/src/stores/storeUserContext.ts @@ -161,6 +161,10 @@ export default class StoreUserContext extends Store { return this.state.acl.samlIdpServiceProvider; } + hasFileTransferAccess() { + return this.state.acl.fileTransferAccess; + } + // hasPrereqAccessToAddAgents checks if user meets the prerequisite // access to add an agent: // - user should be able to create provisioning tokens diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx b/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx index 503383cbdb651..def77da4ec252 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx +++ b/web/packages/teleterm/src/ui/DocumentTerminal/DocumentTerminal.tsx @@ -133,7 +133,12 @@ export function DocumentTerminal(props: { autoFocusDisabled={true} > - + {attempt.status === 'success' && (