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 ca3d5a727ff01..3e82ccdf502e0 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -3014,6 +3014,7 @@ func makeUnifiedResourceRequest(r *http.Request) (*proto.ListUnifiedResourcesReq types.KindWindowsDesktop, types.KindKubernetesCluster, types.KindSAMLIdPServiceProvider, + types.KindGitServer, } } @@ -3140,12 +3141,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) + switch enriched.GetKind() { + case types.KindNode: + logins, err := calculateSSHLogins(identity, enriched.Logins) + if err != nil { + return nil, trace.Wrap(err) + } + unifiedResources = append(unifiedResources, ui.MakeServer(site.GetName(), r, logins, enriched.RequiresRequest)) + case types.KindGitServer: + unifiedResources = append(unifiedResources, ui.MakeGitServer(site.GetName(), r, enriched.RequiresRequest)) } - - unifiedResources = append(unifiedResources, ui.MakeServer(site.GetName(), r, logins, enriched.RequiresRequest)) case types.DatabaseServer: db := ui.MakeDatabase(r.GetDatabase(), accessChecker, h.cfg.DatabaseREPLRegistry, enriched.RequiresRequest) unifiedResources = append(unifiedResources, db) diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 85e55ebbeea8c..752240dd36575 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -1299,6 +1299,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{ @@ -1395,6 +1396,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") @@ -1445,7 +1455,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/git_server.go b/lib/web/ui/git_server.go new file mode 100644 index 0000000000000..b1a89c559a854 --- /dev/null +++ b/lib/web/ui/git_server.go @@ -0,0 +1,80 @@ +/* + * Teleport + * Copyright (C) 2025 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 . + */ + +package ui + +import ( + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/ui" +) + +// GitServer describes a GitServer for webapp +type GitServer struct { + // Kind is the kind of resource. + Kind string `json:"kind"` + // SubKind is a git server subkind such as GitHub + SubKind string `json:"subKind"` + // Name is this server name + Name string `json:"id"` + // ClusterName is this server cluster name + ClusterName string `json:"siteId"` + // Hostname is this server hostname + Hostname string `json:"hostname"` + // Addr is this server ip address + Addr string `json:"addr"` + // Labels is this server list of labels + Labels []ui.Label `json:"tags"` + // 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"` +} + +// GitHubServerMetadata contains metadata for GitHub proxy severs. +type GitHubServerMetadata struct { + Integration string `json:"integration"` + Organization string `json:"organization"` +} + +// MakeGitServer creates a git server object for the web ui +func MakeGitServer(clusterName string, server types.Server, requiresRequest bool) GitServer { + serverLabels := server.GetStaticLabels() + serverCmdLabels := server.GetCmdLabels() + uiLabels := ui.MakeLabelsWithoutInternalPrefixes(serverLabels, ui.TransformCommandLabels(serverCmdLabels)) + + uiServer := GitServer{ + Kind: server.GetKind(), + ClusterName: clusterName, + Labels: uiLabels, + Name: server.GetName(), + Hostname: server.GetHostname(), + Addr: server.GetAddr(), + SubKind: server.GetSubKind(), + RequiresRequest: requiresRequest, + } + + if 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/git_server_test.go b/lib/web/ui/git_server_test.go new file mode 100644 index 0000000000000..41b01ea5ba4d8 --- /dev/null +++ b/lib/web/ui/git_server_test.go @@ -0,0 +1,51 @@ +/* + * Teleport + * Copyright (C) 2025 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 . + */ +package ui + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/ui" +) + +func TestMakeGitServer(t *testing.T) { + server, err := types.NewGitHubServer(types.GitHubServerMetadata{ + Integration: "my-integration", + Organization: "my-org", + }) + require.NoError(t, err) + + expect := GitServer{ + ClusterName: "cluster", + Kind: "git_server", + SubKind: "github", + Addr: "github.com:22", + Name: server.GetName(), + Hostname: "my-org.teleport-github-org", + GitHub: &GitHubServerMetadata{ + Integration: "my-integration", + Organization: "my-org", + }, + // Internal labels get filtered. + Labels: []ui.Label{}, + } + require.Equal(t, expect, MakeGitServer("cluster", server, false)) +} 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 c214fa383b577..0a0fe6c3f8042 100644 --- a/web/packages/design/src/ResourceIcon/icons.ts +++ b/web/packages/design/src/ResourceIcon/icons.ts @@ -126,6 +126,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'; @@ -413,6 +415,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 ab45fd7d8d4b9..1669117c1339e 100644 --- a/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts +++ b/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts @@ -123,6 +123,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 3fc6a366e1f09..237755e753fe0 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx @@ -790,6 +790,8 @@ function getPrettyResourceKind(kind: RequestableResourceKind): string { return 'Namespace'; case 'aws_ic_account_assignment': return 'AWS IAM Identity Center Account Assignment'; + 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 7d4b274dd880c..ca5a031f5db89 100644 --- a/web/packages/shared/components/AccessRequests/NewRequest/resource.ts +++ b/web/packages/shared/components/AccessRequests/NewRequest/resource.ts @@ -46,5 +46,6 @@ export function getEmptyResourceState(): ResourceMap { saml_idp_service_provider: {}, namespace: {}, aws_ic_account_assignment: {}, + git_server: {}, }; } diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx index 405f2af350e02..5c18e12d346ec 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -44,6 +44,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 a475378681988..4c2c0339cf4cc 100644 --- a/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx +++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.story.tsx @@ -32,6 +32,7 @@ import { apps, moreApps } from 'teleport/Apps/fixtures'; import { UrlResourcesParams } from 'teleport/config'; import { databases, moreDatabases } from 'teleport/Databases/fixtures'; import { desktops, moreDesktops } from 'teleport/Desktops/fixtures'; +import { gitServers } from 'teleport/GitServers/fixtures'; import { kubes, moreKubes } from 'teleport/Kubes/fixtures'; import { moreNodes, nodes } from 'teleport/Nodes/fixtures'; import { ResourcesResponse } from 'teleport/services/agents'; @@ -67,6 +68,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 ec4b705a72f93..01ffdc258fcb2 100644 --- a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx +++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx @@ -659,8 +659,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 3eaf6082b4de8..76cdb1baa1ae5 100644 --- a/web/packages/shared/components/UnifiedResources/shared/viewItemsFactory.ts +++ b/web/packages/shared/components/UnifiedResources/shared/viewItemsFactory.ts @@ -20,6 +20,7 @@ import { Application as ApplicationIcon, Database as DatabaseIcon, Desktop as DesktopIcon, + GitHub as GitHubIcon, Kubernetes as KubernetesIcon, Server as ServerIcon, } from 'design/Icon'; @@ -32,6 +33,7 @@ import { UnifiedResourceApp, UnifiedResourceDatabase, UnifiedResourceDesktop, + UnifiedResourceGitServer, UnifiedResourceKube, UnifiedResourceNode, UnifiedResourceUi, @@ -170,6 +172,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': @@ -214,5 +236,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 9c47a0dc64bfd..3d57c312b7637 100644 --- a/web/packages/shared/components/UnifiedResources/types.ts +++ b/web/packages/shared/components/UnifiedResources/types.ts @@ -87,6 +87,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; }; @@ -98,7 +111,8 @@ export type SharedUnifiedResource = { | UnifiedResourceNode | UnifiedResourceKube | UnifiedResourceDesktop - | UnifiedResourceUserGroup; + | UnifiedResourceUserGroup + | UnifiedResourceGitServer; ui: UnifiedResourceUi; }; diff --git a/web/packages/teleport/src/GitServers/ConnectDialog.story.tsx b/web/packages/teleport/src/GitServers/ConnectDialog.story.tsx new file mode 100644 index 0000000000000..6a870b2ced050 --- /dev/null +++ b/web/packages/teleport/src/GitServers/ConnectDialog.story.tsx @@ -0,0 +1,43 @@ +/** + * 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 { 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.tsx b/web/packages/teleport/src/GitServers/ConnectDialog.tsx new file mode 100644 index 0000000000000..71a12f6ce41ac --- /dev/null +++ b/web/packages/teleport/src/GitServers/ConnectDialog.tsx @@ -0,0 +1,106 @@ +/** + * 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 { Box, ButtonSecondary, Link, Text } from 'design'; +import Dialog, { + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from 'design/Dialog'; +import { TextSelectCopy } from 'shared/components/TextSelectCopy'; + +import { generateTshLoginCommand } from 'teleport/lib/util'; +import { AuthType } from 'teleport/services/user'; + +export function ConnectDialog({ + username, + clusterId, + organization, + onClose, + authType, + accessRequestId, +}: { + organization: string; + onClose: () => void; + username: string; + clusterId: string; + authType: AuthType; + accessRequestId?: string; +}) { + const repoURL = `https://github.com/orgs/${organization}/repositories`; + const title = `Use 'git' for GitHub Organization '${organization}'`; + return ( + ({ + maxWidth: '600px', + width: '100%', + })} + disableEscapeKeyDown={false} + onClose={onClose} + open={true} + > + + {title} + + + + + Step 1 + + {' - Log in to Teleport'} + + + + + Step 2 + + {' - Clone or configure a repository'} +
+ {'To clone a new repository, find the SSH url of the repository on '} + + github.com + + {', and then'} + + To configure an existing Git repository, go to the repository and then + +
+ + Once the repository is cloned or configured, use 'git' as normal. + +
+ + Close + +
+ ); +} diff --git a/web/packages/teleport/src/GitServers/fixtures.ts b/web/packages/teleport/src/GitServers/fixtures.ts new file mode 100644 index 0000000000000..2c20c35341aeb --- /dev/null +++ b/web/packages/teleport/src/GitServers/fixtures.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 'web/packages/teleport/src/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/GitServers/index.ts b/web/packages/teleport/src/GitServers/index.ts new file mode 100644 index 0000000000000..e2c8486f53824 --- /dev/null +++ b/web/packages/teleport/src/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 { ConnectDialog } from './ConnectDialog'; diff --git a/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx b/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx index 01a880c3b82ea..90c43bde0d99b 100644 --- a/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx +++ b/web/packages/teleport/src/UnifiedResources/ResourceActionButton.tsx @@ -17,6 +17,7 @@ */ import React, { useState } from 'react'; +import { GitServer } from 'web/packages/teleport/src/services/gitServers'; import { ButtonBorder, ButtonWithMenu, MenuItem } from 'design'; import { AwsLaunchButton } from 'shared/components/AwsLaunchButton'; @@ -31,6 +32,7 @@ import cfg from 'teleport/config'; import DbConnectDialog from 'teleport/Databases/ConnectDialog'; import type { ResourceSpec } from 'teleport/Discover/SelectResource/types'; import { ResourceKind } from 'teleport/Discover/Shared'; +import { ConnectDialog as GitServerConnectDialog } from 'teleport/GitServers'; import KubeConnectDialog from 'teleport/Kubes/ConnectDialog'; import { openNewTab } from 'teleport/lib/util'; import { useSamlAppAction } from 'teleport/SamlApplications/useSamlAppActions'; @@ -60,6 +62,8 @@ export const ResourceActionButton = ({ resource }: Props) => { return ; case 'windows_desktop': return ; + case 'git_server': + return ; default: return null; } @@ -351,6 +355,40 @@ 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.organization; + 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 c10fd8bdab04c..9626a61e14d1a 100644 --- a/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx +++ b/web/packages/teleport/src/UnifiedResources/UnifiedResources.tsx @@ -93,6 +93,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 48445672325d2..f23719526b9bc 100644 --- a/web/packages/teleport/src/mocks/contexts.ts +++ b/web/packages/teleport/src/mocks/contexts.ts @@ -76,6 +76,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 c15b72a73529b..4fcfcac74b168 100644 --- a/web/packages/teleport/src/services/agents/types.ts +++ b/web/packages/teleport/src/services/agents/types.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { GitServer } from 'web/packages/teleport/src/services/gitServers'; + import type { Platform } from 'design/platform'; import { IncludedResourceMode } from 'shared/components/UnifiedResources'; @@ -24,9 +26,9 @@ import { Database } from 'teleport/services/databases'; import { Desktop } from 'teleport/services/desktops'; import { Kube } from 'teleport/services/kube'; import { Node } from 'teleport/services/nodes'; +import { UserGroup } from 'teleport/services/userGroups'; import type { MfaChallengeResponse } from '../mfa'; -import { UserGroup } from '../userGroups'; export type UnifiedResource = | App @@ -34,7 +36,8 @@ export type UnifiedResource = | Node | Kube | Desktop - | UserGroup; + | UserGroup + | GitServer; export type UnifiedResourceKind = UnifiedResource['kind']; @@ -88,7 +91,8 @@ export type ResourceIdKind = | 'user_group' | 'windows_desktop' | 'saml_idp_service_provider' - | 'aws_ic_account_assignment'; + | 'aws_ic_account_assignment' + | 'git_server'; export type AccessRequestScope = | 'my_requests' 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..7b92b6fa470d7 --- /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 { GitHubMetadata, GitServer } 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..264ae717000de --- /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..608c0c0b1a5dd 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 cfbe71fe43f6a..5442173a22a6e 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 f909670b9859f..1b8f20dcf216d 100644 --- a/web/packages/teleport/src/stores/storeUserContext.ts +++ b/web/packages/teleport/src/stores/storeUserContext.ts @@ -258,4 +258,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 3cacff180e27b..59406c4cafa68 100644 --- a/web/packages/teleport/src/teleportContext.tsx +++ b/web/packages/teleport/src/teleportContext.tsx @@ -242,6 +242,9 @@ 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 +285,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 69e909cd5011c..daa3758e3919b 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.