From acaca65e295aa7c22cfa0684c5b107fe09cdbe36 Mon Sep 17 00:00:00 2001 From: STeve Huang Date: Fri, 29 Nov 2024 15:10:14 -0500 Subject: [PATCH] GitHub proxy part 4.5: git server in UI --- lib/services/useracl.go | 4 + lib/services/useracl_test.go | 1 + lib/web/apiserver.go | 14 ++- lib/web/apiserver_test.go | 12 +- lib/web/ui/server.go | 17 +++ lib/web/ui/server_test.go | 59 ++++++++++ .../src/ResourceIcon/assets/git-dark.svg | 1 + .../src/ResourceIcon/assets/git-light.svg | 1 + web/packages/design/src/ResourceIcon/icons.ts | 4 + .../src/ResourceIcon/resourceIconSpecs.ts | 1 + .../RequestCheckout/RequestCheckout.tsx | 2 + .../AccessRequests/NewRequest/resource.ts | 1 + .../UnifiedResources/FilterPanel.tsx | 1 + .../UnifiedResources.story.tsx | 2 + .../UnifiedResources/UnifiedResources.tsx | 4 +- .../shared/viewItemsFactory.ts | 24 ++++ .../components/UnifiedResources/types.ts | 16 ++- .../ConnectDialog/ConnectDialog.story.tsx | 45 ++++++++ .../ConnectDialog/ConnectDialog.tsx | 104 ++++++++++++++++++ .../src/GitServers/ConnectDialog/index.ts | 20 ++++ .../teleport/src/GitServers/fixtures/index.ts | 34 ++++++ .../UnifiedResources/ResourceActionButton.tsx | 40 +++++++ .../src/UnifiedResources/UnifiedResources.tsx | 4 + web/packages/teleport/src/mocks/contexts.ts | 1 + .../teleport/src/services/agents/types.ts | 8 +- .../teleport/src/services/gitservers/index.ts | 19 ++++ .../src/services/gitservers/makeGitServer.ts | 45 ++++++++ .../teleport/src/services/gitservers/types.ts | 35 ++++++ .../services/resources/makeUnifiedResource.ts | 4 + .../teleport/src/services/user/makeAcl.ts | 2 + .../teleport/src/services/user/types.ts | 1 + .../teleport/src/services/user/user.test.ts | 7 ++ .../teleport/src/stores/storeUserContext.ts | 4 + web/packages/teleport/src/teleportContext.tsx | 3 + web/packages/teleport/src/types.ts | 1 + 35 files changed, 530 insertions(+), 11 deletions(-) create mode 100644 web/packages/design/src/ResourceIcon/assets/git-dark.svg create mode 100644 web/packages/design/src/ResourceIcon/assets/git-light.svg create mode 100644 web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.story.tsx create mode 100644 web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.tsx create mode 100644 web/packages/teleport/src/GitServers/ConnectDialog/index.ts create mode 100644 web/packages/teleport/src/GitServers/fixtures/index.ts create mode 100644 web/packages/teleport/src/services/gitservers/index.ts create mode 100644 web/packages/teleport/src/services/gitservers/makeGitServer.ts create mode 100644 web/packages/teleport/src/services/gitservers/types.ts diff --git a/lib/services/useracl.go b/lib/services/useracl.go index 4a6c39b554ee5..05ff20400c2aa 100644 --- a/lib/services/useracl.go +++ b/lib/services/useracl.go @@ -120,6 +120,8 @@ type UserACL struct { Contact ResourceAccess `json:"contact"` // FileTransferAccess defines the ability to perform remote file operations via SCP or SFTP FileTransferAccess bool `json:"fileTransferAccess"` + // GitServers defines access to Git servers. + GitServers ResourceAccess `json:"gitServers"` } func hasAccess(roleSet RoleSet, ctx *Context, kind string, verbs ...string) bool { @@ -164,6 +166,7 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des desktopAccess := newAccess(userRoles, ctx, types.KindWindowsDesktop) cnDiagnosticAccess := newAccess(userRoles, ctx, types.KindConnectionDiagnostic) samlIdpServiceProviderAccess := newAccess(userRoles, ctx, types.KindSAMLIdPServiceProvider) + gitServersAccess := newAccess(userRoles, ctx, types.KindGitServer) // active sessions are a special case - if a user's role set has any join_sessions // policies then the ACL must permit showing active sessions @@ -266,5 +269,6 @@ func NewUserACL(user types.User, userRoles RoleSet, features proto.Features, des AccessGraphSettings: accessGraphSettings, Contact: contact, FileTransferAccess: fileTransferAccess, + GitServers: gitServersAccess, } } diff --git a/lib/services/useracl_test.go b/lib/services/useracl_test.go index 9eb19e199007e..a9e7a65c147e3 100644 --- a/lib/services/useracl_test.go +++ b/lib/services/useracl_test.go @@ -109,6 +109,7 @@ func TestNewUserACL(t *testing.T) { require.Empty(t, cmp.Diff(userContext.License, denied)) require.Empty(t, cmp.Diff(userContext.Download, denied)) require.Empty(t, cmp.Diff(userContext.Contact, allowedRW)) + require.Empty(t, cmp.Diff(userContext.GitServers, denied)) // test enabling of the 'Use' verb require.Empty(t, cmp.Diff(userContext.Integrations, ResourceAccess{true, true, true, true, true, true})) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index d674d094248c8..3ae47b2376070 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -2948,6 +2948,7 @@ func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesReq types.KindWindowsDesktop, types.KindKubernetesCluster, types.KindSAMLIdPServiceProvider, + types.KindGitServer, } } @@ -3077,11 +3078,16 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt for _, enriched := range page { switch r := enriched.ResourceWithLabels.(type) { case types.Server: - logins, err := calculateSSHLogins(identity, enriched.Logins) - if err != nil { - return nil, trace.Wrap(err) + var logins []string + switch enriched.GetKind() { + case types.KindNode: + logins, err = calculateSSHLogins(identity, enriched.Logins) + if err != nil { + return nil, trace.Wrap(err) + } + case types.KindGitServer: + break } - unifiedResources = append(unifiedResources, ui.MakeServer(site.GetName(), r, logins, enriched.RequiresRequest)) case types.DatabaseServer: if !hasFetchedDBUsersAndNames { diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 3a701fe65a26a..b0d1d1e2388d3 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -1281,6 +1281,7 @@ func TestUnifiedResourcesGet(t *testing.T) { role := defaultRoleForNewUser(&types.UserV2{Metadata: types.Metadata{Name: username}}, loginUser) role.SetAWSRoleARNs(types.Allow, []string{"arn:aws:iam::999999999999:role/ProdInstance"}) role.SetAppLabels(types.Allow, types.Labels{"env": []string{"prod"}}) + role.SetGitHubPermissions(types.Allow, []types.GitHubPermission{{Organizations: []string{types.Wildcard}}}) // This role is used to test that DevInstance AWS Role is only available to AppServices that have env:dev label. roleForDev, err := types.NewRole("dev-access", types.RoleSpecV6{ @@ -1377,6 +1378,15 @@ func TestUnifiedResourcesGet(t *testing.T) { err = env.server.Auth().UpsertWindowsDesktop(context.Background(), win) require.NoError(t, err) + // add git server + gitServer, err := types.NewGitHubServer(types.GitHubServerMetadata{ + Organization: "org1", + Integration: "org1", + }) + require.NoError(t, err) + _, err = env.server.Auth().GitServers.UpsertGitServer(context.Background(), gitServer) + require.NoError(t, err) + clusterName := env.server.ClusterName() endpoint := pack.clt.Endpoint("webapi", "sites", clusterName, "resources") @@ -1427,7 +1437,7 @@ func TestUnifiedResourcesGet(t *testing.T) { require.NoError(t, err) res = clusterNodesGetResponse{} require.NoError(t, json.Unmarshal(re.Bytes(), &res)) - require.Len(t, res.Items, 10) + require.Len(t, res.Items, 11) require.Equal(t, "", res.StartKey) // Only list valid AWS Roles for AWS Apps diff --git a/lib/web/ui/server.go b/lib/web/ui/server.go index 9921307c48134..ebdf4d34fdbc0 100644 --- a/lib/web/ui/server.go +++ b/lib/web/ui/server.go @@ -54,6 +54,8 @@ type Server struct { AWS *AWSMetadata `json:"aws,omitempty"` // RequireRequest indicates if a returned resource is only accessible after an access request RequiresRequest bool `json:"requiresRequest,omitempty"` + // GitHub contains metadata for GitHub proxy severs. + GitHub *GitHubServerMetadata `json:"github,omitempty"` } // AWSMetadata describes the AWS metadata for instances hosted in AWS. @@ -67,6 +69,12 @@ type AWSMetadata struct { SubnetID string `json:"subnetId"` } +// GitHubServerMetadata contains metadata for GitHub proxy severs. +type GitHubServerMetadata struct { + Integration string `json:"integration"` + Organization string `json:"organization"` +} + // MakeServer creates a server object for the web ui func MakeServer(clusterName string, server types.Server, logins []string, requiresRequest bool) Server { serverLabels := server.GetStaticLabels() @@ -98,6 +106,15 @@ func MakeServer(clusterName string, server types.Server, logins []string, requir } } + if server.GetKind() == types.KindGitServer && + server.GetSubKind() == types.SubKindGitHub { + if github := server.GetGitHub(); github != nil { + uiServer.GitHub = &GitHubServerMetadata{ + Integration: github.Integration, + Organization: github.Organization, + } + } + } return uiServer } diff --git a/lib/web/ui/server_test.go b/lib/web/ui/server_test.go index 9af621cf09557..e5795e92414f8 100644 --- a/lib/web/ui/server_test.go +++ b/lib/web/ui/server_test.go @@ -590,3 +590,62 @@ func TestSortedLabels(t *testing.T) { }) } } + +func TestMakeServer(t *testing.T) { + tests := []struct { + name string + inputServer types.Server + inputLogins []string + output Server + }{ + { + name: "git server", + inputServer: makeGitServer(t, "org1"), + output: Server{ + ClusterName: "cluster", + Kind: "git_server", + SubKind: "github", + Name: "org1", + Hostname: "org1.github-org", + GitHub: &GitHubServerMetadata{ + Integration: "org1", + Organization: "org1", + }, + // Internal labels get filtered. + Labels: []ui.Label{}, + }, + }, + { + name: "node", + inputServer: makeTestServer(t, "server1", map[string]string{"env": "dev"}), + inputLogins: []string{"alice"}, + output: Server{ + ClusterName: "cluster", + Kind: "node", + SubKind: "teleport", + Name: "server1", + SSHLogins: []string{"alice"}, + Labels: []ui.Label{{ + Name: "env", + Value: "dev", + }}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, MakeServer("cluster", test.inputServer, test.inputLogins, false)) + }) + } +} + +func makeGitServer(t *testing.T, org string) types.Server { + t.Helper() + server, err := types.NewGitHubServer(types.GitHubServerMetadata{ + Integration: org, + Organization: org, + }) + require.NoError(t, err) + server.SetName(org) + return server +} diff --git a/web/packages/design/src/ResourceIcon/assets/git-dark.svg b/web/packages/design/src/ResourceIcon/assets/git-dark.svg new file mode 100644 index 0000000000000..cb1a374e8badc --- /dev/null +++ b/web/packages/design/src/ResourceIcon/assets/git-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/packages/design/src/ResourceIcon/assets/git-light.svg b/web/packages/design/src/ResourceIcon/assets/git-light.svg new file mode 100644 index 0000000000000..5bf444b9be0ca --- /dev/null +++ b/web/packages/design/src/ResourceIcon/assets/git-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/packages/design/src/ResourceIcon/icons.ts b/web/packages/design/src/ResourceIcon/icons.ts index 8595076221a6c..79d45a48ef765 100644 --- a/web/packages/design/src/ResourceIcon/icons.ts +++ b/web/packages/design/src/ResourceIcon/icons.ts @@ -122,6 +122,8 @@ import g2 from './assets/g2.svg'; import gable from './assets/gable.svg'; import gemDark from './assets/gem-dark.svg'; import gemLight from './assets/gem-light.svg'; +import gitDark from './assets/git-dark.svg'; +import gitLight from './assets/git-light.svg'; import githubDark from './assets/github-dark.svg'; import githubLight from './assets/github-light.svg'; import gitlab from './assets/gitlab.svg'; @@ -408,6 +410,8 @@ export { gable, gemDark, gemLight, + gitDark, + gitLight, githubDark, githubLight, gitlab, diff --git a/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts b/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts index 80007e3a1e4c8..82a10c062fa52 100644 --- a/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts +++ b/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts @@ -121,6 +121,7 @@ export const resourceIconSpecs = { g2: forAllThemes(i.g2), gable: forAllThemes(i.gable), gem: { dark: i.gemDark, light: i.gemLight }, + git: { dark: i.gitDark, light: i.gitLight }, github: { dark: i.githubDark, light: i.githubLight }, gitlab: forAllThemes(i.gitlab), gmail: forAllThemes(i.gmail), diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx index fd4bc1578869d..ac6c883e5a07d 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx @@ -791,6 +791,8 @@ function getPrettyResourceKind(kind: RequestableResourceKind): string { return 'SAML Application'; case 'namespace': return 'Namespace'; + case 'git_server': + return 'Git'; default: kind satisfies never; return kind; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/resource.ts b/web/packages/shared/components/AccessRequests/NewRequest/resource.ts index f56ad58110d25..38f1a4b678bfd 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/resource.ts +++ b/web/packages/shared/components/AccessRequests/NewRequest/resource.ts @@ -45,5 +45,6 @@ export function getEmptyResourceState(): ResourceMap { role: {}, saml_idp_service_provider: {}, namespace: {}, + git_server: {}, }; } diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx index d470abe9a9e9e..680b27d2ea75f 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -47,6 +47,7 @@ const kindToLabel: Record = { kube_cluster: 'Kubernetes', node: 'Server', user_group: 'User group', + git_server: 'Git', }; const sortFieldOptions = [ diff --git a/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx b/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx index cd67aad58dd29..13d267254f9ac 100644 --- a/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx +++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx @@ -25,6 +25,7 @@ import { databases, moreDatabases } from 'teleport/Databases/fixtures'; import { kubes, moreKubes } from 'teleport/Kubes/fixtures'; import { desktops, moreDesktops } from 'teleport/Desktops/fixtures'; import { moreNodes, nodes } from 'teleport/Nodes/fixtures'; +import { gitServers } from 'teleport/GitServers/fixtures'; import { UrlResourcesParams } from 'teleport/config'; import { ResourcesResponse } from 'teleport/services/agents'; @@ -70,6 +71,7 @@ const allResources = [ ...moreKubes, ...moreDesktops, ...moreNodes, + ...gitServers, ]; const story = ({ diff --git a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx index a9f0699018d42..65596eeb609ed 100644 --- a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx +++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx @@ -660,8 +660,8 @@ function getResourcePinningSupport( function generateUnifiedResourceKey( resource: SharedUnifiedResource['resource'] ): string { - if (resource.kind === 'node') { - return `${resource.hostname}/${resource.id}/node`.toLowerCase(); + if (resource.kind === 'node' || resource.kind == 'git_server') { + return `${resource.hostname}/${resource.id}/${resource.kind}`.toLowerCase(); } return `${resource.name}/${resource.kind}`.toLowerCase(); } diff --git a/web/packages/shared/components/UnifiedResources/shared/viewItemsFactory.ts b/web/packages/shared/components/UnifiedResources/shared/viewItemsFactory.ts index 9a70b5951a329..6a0dba7a804e6 100644 --- a/web/packages/shared/components/UnifiedResources/shared/viewItemsFactory.ts +++ b/web/packages/shared/components/UnifiedResources/shared/viewItemsFactory.ts @@ -22,6 +22,7 @@ import { Kubernetes as KubernetesIcon, Server as ServerIcon, Desktop as DesktopIcon, + GitHub as GitHubIcon, } from 'design/Icon'; import { ResourceIconName } from 'design/ResourceIcon'; @@ -37,6 +38,7 @@ import { UnifiedResourceDesktop, UnifiedResourceKube, UnifiedResourceUserGroup, + UnifiedResourceGitServer, SharedUnifiedResource, } from '../types'; @@ -172,6 +174,26 @@ export function makeUnifiedResourceViewItemUserGroup( }; } +export function makeUnifiedResourceViewItemGitServer( + resource: UnifiedResourceGitServer, + ui: UnifiedResourceUi +): UnifiedResourceViewItem { + return { + name: resource.github ? resource.github.organization : resource.hostname, + SecondaryIcon: GitHubIcon, + primaryIconName: 'git', + ActionButton: ui.ActionButton, + labels: resource.labels, + cardViewProps: { + primaryDesc: 'GitHub Organization', + }, + listViewProps: { + resourceType: 'GitHub Organization', + }, + requiresRequest: resource.requiresRequest, + }; +} + function formatNodeSubKind(subKind: NodeSubKind): string { switch (subKind) { case 'openssh-ec2-ice': @@ -216,5 +238,7 @@ export function mapResourceToViewItem({ resource, ui }: SharedUnifiedResource) { return makeUnifiedResourceViewItemDesktop(resource, ui); case 'user_group': return makeUnifiedResourceViewItemUserGroup(resource, ui); + case 'git_server': + return makeUnifiedResourceViewItemGitServer(resource, ui); } } diff --git a/web/packages/shared/components/UnifiedResources/types.ts b/web/packages/shared/components/UnifiedResources/types.ts index 22c0c52547181..bb6d0e1fa66fa 100644 --- a/web/packages/shared/components/UnifiedResources/types.ts +++ b/web/packages/shared/components/UnifiedResources/types.ts @@ -85,6 +85,19 @@ export type UnifiedResourceUserGroup = { requiresRequest?: boolean; }; +export interface UnifiedResourceGitServer { + kind: 'git_server'; + id: string; + hostname: string; + labels: ResourceLabel[]; + subKind: 'github'; + github?: { + organization: string; + integration: string; + }; + requiresRequest?: boolean; +} + export type UnifiedResourceUi = { ActionButton: React.ReactElement; }; @@ -96,7 +109,8 @@ export type SharedUnifiedResource = { | UnifiedResourceNode | UnifiedResourceKube | UnifiedResourceDesktop - | UnifiedResourceUserGroup; + | UnifiedResourceUserGroup + | UnifiedResourceGitServer; ui: UnifiedResourceUi; }; diff --git a/web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.story.tsx b/web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.story.tsx new file mode 100644 index 0000000000000..335a5970828c2 --- /dev/null +++ b/web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.story.tsx @@ -0,0 +1,45 @@ +/** + * Teleport + * Copyright (C) 2023 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 React from 'react'; + +import ConnectDialog from './ConnectDialog'; + +export default { + title: 'Teleport/GitServers/Connect', +}; + +export const ConnectGitHub = () => ( + null} + authType="local" + /> +); + +export const ConnectGitHubSSO = () => ( + null} + authType="sso" + /> +); diff --git a/web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.tsx b/web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.tsx new file mode 100644 index 0000000000000..82f2152f9a911 --- /dev/null +++ b/web/packages/teleport/src/GitServers/ConnectDialog/ConnectDialog.tsx @@ -0,0 +1,104 @@ +/** + * Teleport + * Copyright (C) 2023 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 React from 'react'; +import { Text, Box, ButtonSecondary, Link } from 'design'; +import Dialog, { + DialogHeader, + DialogTitle, + DialogContent, + DialogFooter, +} from 'design/Dialog'; +import { TextSelectCopy } from 'shared/components/TextSelectCopy'; + +import { AuthType } from 'teleport/services/user'; +import { generateTshLoginCommand } from 'teleport/lib/util'; + +export default function ConnectDialog({ + username, + clusterId, + organization, + onClose, + authType, + accessRequestId, +}: Props) { + let repoURL = `https://github.com/orgs/${organization}/repositories`; + let title = `Use 'git' for GitHub Organization '${organization}'`; + return ( + ({ + maxWidth: '600px', + width: '100%', + })} + disableEscapeKeyDown={false} + onClose={onClose} + open={true} + > + + {title} + + + + + Step 1 + + {' - Login to Teleport'} + + + + + Step 2 + + {' - Connect'} +
+ {'To clone a new repository, find the SSH url of the repository on '} + + github.com + + {' then'} + `} /> + {'To configure an existing Git repository, go to the repository then'} + +
+ + {`Once the repository is cloned or configured, use 'git' as normal.`} + +
+ + Close + +
+ ); +} + +export type Props = { + organization: string; + onClose: () => void; + username: string; + clusterId: string; + authType: AuthType; + accessRequestId?: string; +}; diff --git a/web/packages/teleport/src/GitServers/ConnectDialog/index.ts b/web/packages/teleport/src/GitServers/ConnectDialog/index.ts new file mode 100644 index 0000000000000..7d524172d41ec --- /dev/null +++ b/web/packages/teleport/src/GitServers/ConnectDialog/index.ts @@ -0,0 +1,20 @@ +/** + * 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 ConnectDialog from './ConnectDialog'; +export default ConnectDialog; diff --git a/web/packages/teleport/src/GitServers/fixtures/index.ts b/web/packages/teleport/src/GitServers/fixtures/index.ts new file mode 100644 index 0000000000000..ff487ee0f0a13 --- /dev/null +++ b/web/packages/teleport/src/GitServers/fixtures/index.ts @@ -0,0 +1,34 @@ +/** + * 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 { GitServer } from 'teleport/services/gitservers'; + +export const gitServers: GitServer[] = [ + { + kind: 'git_server', + id: '00000000-0000-0000-0000-000000000000', + clusterId: 'im-a-cluster', + hostname: 'my-org.github-org', + subKind: 'github', + labels: [], + github: { + organization: 'my-org', + integration: 'my-org', + }, + }, +]; diff --git a/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx b/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx index ab51c11b2c4c4..814e5048c211f 100644 --- a/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx +++ b/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx @@ -32,8 +32,10 @@ import { Database } from 'teleport/services/databases'; import { openNewTab } from 'teleport/lib/util'; import { Kube } from 'teleport/services/kube'; import { Desktop } from 'teleport/services/desktops'; +import { GitServer } from 'teleport/services/gitservers'; import DbConnectDialog from 'teleport/Databases/ConnectDialog'; import KubeConnectDialog from 'teleport/Kubes/ConnectDialog'; +import GitServerConnectDialog from 'teleport/GitServers/ConnectDialog'; import useStickyClusterId from 'teleport/useStickyClusterId'; import { Node, sortNodeLogins } from 'teleport/services/nodes'; import { App } from 'teleport/services/apps'; @@ -59,6 +61,8 @@ export const ResourceActionButton = ({ resource }: Props) => { return ; case 'windows_desktop': return ; + case 'git_server': + return ; default: return null; } @@ -327,6 +331,42 @@ const KubeConnect = ({ kube }: { kube: Kube }) => { ); }; +function GitServerConnect({ gitserver }: { gitserver: GitServer }) { + const ctx = useTeleport(); + const { clusterId } = useStickyClusterId(); + const [open, setOpen] = useState(false); + const organization = gitserver.github + ? gitserver.github.organization + : undefined; + const username = ctx.storeUser.state.username; + const authType = ctx.storeUser.state.authType; + const accessRequestId = ctx.storeUser.getAccessRequestId(); + return ( + <> + { + setOpen(true); + }} + > + Connect + + {open && ( + setOpen(false)} + authType={authType} + accessRequestId={accessRequestId} + /> + )} + + ); +} + const makeNodeOptions = (clusterId: string, node: Node | undefined) => { const nodeLogins = node?.sshLogins || []; const logins = sortNodeLogins(nodeLogins); diff --git a/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx b/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx index 0e2b314736e12..8b0fe89e3c8d3 100644 --- a/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx +++ b/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx @@ -95,6 +95,10 @@ const getAvailableKindsWithAccess = (flags: FeatureFlags): FilterKind[] => { kind: 'windows_desktop', disabled: !flags.desktops, }, + { + kind: 'git_server', + disabled: !flags.gitServers, + }, ]; }; diff --git a/web/packages/teleport/src/mocks/contexts.ts b/web/packages/teleport/src/mocks/contexts.ts index 2d1378f964fd3..afc31a15d6c7d 100644 --- a/web/packages/teleport/src/mocks/contexts.ts +++ b/web/packages/teleport/src/mocks/contexts.ts @@ -77,6 +77,7 @@ export const allAccessAcl: Acl = { accessMonitoringRule: fullAccess, discoverConfigs: fullAccess, contacts: fullAccess, + gitServers: fullAccess, }; export function getAcl(cfg?: { noAccess: boolean }) { diff --git a/web/packages/teleport/src/services/agents/types.ts b/web/packages/teleport/src/services/agents/types.ts index 767f43b9af79d..81a3a31638bf7 100644 --- a/web/packages/teleport/src/services/agents/types.ts +++ b/web/packages/teleport/src/services/agents/types.ts @@ -23,8 +23,8 @@ import { Database } from 'teleport/services/databases'; import { Node } from 'teleport/services/nodes'; import { Kube } from 'teleport/services/kube'; import { Desktop } from 'teleport/services/desktops'; - -import { UserGroup } from '../userGroups'; +import { UserGroup } from 'teleport/services/userGroups'; +import { GitServer } from 'teleport/services/gitservers'; import type { MfaAuthnResponse } from '../mfa'; import type { Platform } from 'design/platform'; @@ -35,7 +35,8 @@ export type UnifiedResource = | Node | Kube | Desktop - | UserGroup; + | UserGroup + | GitServer; export type UnifiedResourceKind = UnifiedResource['kind']; @@ -88,6 +89,7 @@ export type ResourceIdKind = | 'kube_cluster' | 'user_group' | 'windows_desktop' + | 'git_server' | 'saml_idp_service_provider'; export type AccessRequestScope = diff --git a/web/packages/teleport/src/services/gitservers/index.ts b/web/packages/teleport/src/services/gitservers/index.ts new file mode 100644 index 0000000000000..d69acdb0a561f --- /dev/null +++ b/web/packages/teleport/src/services/gitservers/index.ts @@ -0,0 +1,19 @@ +/** + * 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 . + */ + +export * from './types'; diff --git a/web/packages/teleport/src/services/gitservers/makeGitServer.ts b/web/packages/teleport/src/services/gitservers/makeGitServer.ts new file mode 100644 index 0000000000000..b08f74ef17010 --- /dev/null +++ b/web/packages/teleport/src/services/gitservers/makeGitServer.ts @@ -0,0 +1,45 @@ +/** + * 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 { GitServer, GitHubMetadata } from './types'; + +export default function makeGitServer(json: any): GitServer { + json = json ?? {}; + const { id, siteId, subKind, hostname, tags, github, requiresRequest } = json; + + return { + kind: 'git_server', + id, + subKind, + clusterId: siteId, + hostname, + labels: tags ?? [], + requiresRequest, + github: github ? makeGitHubMetadata(github) : undefined, + }; +} + +function makeGitHubMetadata(json: any): GitHubMetadata { + json = json ?? {}; + const { integration, organization } = json; + + return { + integration, + organization, + }; +} diff --git a/web/packages/teleport/src/services/gitservers/types.ts b/web/packages/teleport/src/services/gitservers/types.ts new file mode 100644 index 0000000000000..29d93834058d9 --- /dev/null +++ b/web/packages/teleport/src/services/gitservers/types.ts @@ -0,0 +1,35 @@ +/** + * 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 { ResourceLabel } from 'teleport/services/agents'; + +export interface GitServer { + kind: 'git_server'; + id: string; + clusterId: string; + hostname: string; + labels: ResourceLabel[]; + subKind: 'github'; + github?: GitHubMetadata; + requiresRequest?: boolean; +} + +export type GitHubMetadata = { + integration: string; + organization: string; +}; diff --git a/web/packages/teleport/src/services/resources/makeUnifiedResource.ts b/web/packages/teleport/src/services/resources/makeUnifiedResource.ts index 0dc4072c92719..13b28d10483e6 100644 --- a/web/packages/teleport/src/services/resources/makeUnifiedResource.ts +++ b/web/packages/teleport/src/services/resources/makeUnifiedResource.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import makeGitServer from 'teleport/services/gitservers/makeGitServer'; + import { UnifiedResource, UnifiedResourceKind } from '../agents'; import makeApp from '../apps/makeApps'; import { makeDatabase } from '../databases/makeDatabase'; @@ -37,6 +39,8 @@ export function makeUnifiedResource(json: any): UnifiedResource { return makeNode(json); case 'windows_desktop': return makeDesktop(json); + case 'git_server': + return makeGitServer(json); default: throw new Error(`Unknown unified resource kind: "${json.kind}"`); } diff --git a/web/packages/teleport/src/services/user/makeAcl.ts b/web/packages/teleport/src/services/user/makeAcl.ts index 560add4279e31..18265162d3c9c 100644 --- a/web/packages/teleport/src/services/user/makeAcl.ts +++ b/web/packages/teleport/src/services/user/makeAcl.ts @@ -81,6 +81,7 @@ export function makeAcl(json): Acl { const discoverConfigs = json.discoverConfigs || defaultAccess; const contacts = json.contact || defaultAccess; + const gitServers = json.gitServers || defaultAccess; return { accessList, @@ -121,6 +122,7 @@ export function makeAcl(json): Acl { discoverConfigs, contacts, fileTransferAccess, + gitServers, }; } diff --git a/web/packages/teleport/src/services/user/types.ts b/web/packages/teleport/src/services/user/types.ts index 37239dcd8d34d..188851625b7b9 100644 --- a/web/packages/teleport/src/services/user/types.ts +++ b/web/packages/teleport/src/services/user/types.ts @@ -109,6 +109,7 @@ export interface Acl { accessMonitoringRule: Access; contacts: Access; fileTransferAccess: boolean; + gitServers: Access; } // 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 f734942a9d4fa..19346907c4c39 100644 --- a/web/packages/teleport/src/services/user/user.test.ts +++ b/web/packages/teleport/src/services/user/user.test.ts @@ -289,6 +289,13 @@ test('undefined values in context response gives proper default values', async ( desktopSessionRecordingEnabled: true, directorySharingEnabled: true, fileTransferAccess: true, + gitServers: { + list: false, + read: false, + edit: false, + create: false, + remove: false, + }, }; expect(response).toEqual({ diff --git a/web/packages/teleport/src/stores/storeUserContext.ts b/web/packages/teleport/src/stores/storeUserContext.ts index e50f9fb9d6e61..8d2dfa7805e4d 100644 --- a/web/packages/teleport/src/stores/storeUserContext.ts +++ b/web/packages/teleport/src/stores/storeUserContext.ts @@ -259,4 +259,8 @@ export default class StoreUserContext extends Store { getContactsAccess() { return this.state.acl.contacts; } + + getGitServersAccess() { + return this.state.acl.gitServers; + } } diff --git a/web/packages/teleport/src/teleportContext.tsx b/web/packages/teleport/src/teleportContext.tsx index 1ba4c3fde4380..43c7f4dd7c1bd 100644 --- a/web/packages/teleport/src/teleportContext.tsx +++ b/web/packages/teleport/src/teleportContext.tsx @@ -242,6 +242,8 @@ class TeleportContext implements types.Context { addBots: userContext.getBotsAccess().create, editBots: userContext.getBotsAccess().edit, removeBots: userContext.getBotsAccess().remove, + gitServers: userContext.getGitServersAccess().list && + userContext.getGitServersAccess().read, }; } } @@ -282,6 +284,7 @@ export const disabledFeatureFlags: types.FeatureFlags = { listBots: false, editBots: false, removeBots: false, + gitServers: false }; export default TeleportContext; diff --git a/web/packages/teleport/src/types.ts b/web/packages/teleport/src/types.ts index 356dc95027ee1..a62f8604befdc 100644 --- a/web/packages/teleport/src/types.ts +++ b/web/packages/teleport/src/types.ts @@ -206,6 +206,7 @@ export interface FeatureFlags { addBots: boolean; editBots: boolean; removeBots: boolean; + gitServers: boolean; } // LockedFeatures are used for determining which features are disabled in the user's cluster.