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' && (