diff --git a/web/packages/design/src/Tabs/Tabs.ts b/web/packages/design/src/Tabs/Tabs.ts new file mode 100644 index 0000000000000..b8e7a3dd15826 --- /dev/null +++ b/web/packages/design/src/Tabs/Tabs.ts @@ -0,0 +1,58 @@ +/** + * 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 styled from 'styled-components'; + +import { NavLink } from 'react-router-dom'; + +export const TabsContainer = styled.div` + position: relative; + display: flex; + gap: ${p => p.theme.space[5]}px; + align-items: center; + padding: 0 ${p => p.theme.space[5]}px; + border-bottom: 1px solid ${p => p.theme.colors.spotBackground[0]}; +`; + +export const TabContainer = styled(NavLink)<{ selected?: boolean }>` + padding: ${p => p.theme.space[1] + p.theme.space[2]}px + ${p => p.theme.space[2]}px; + position: relative; + cursor: pointer; + z-index: 2; + opacity: ${p => (p.selected ? 1 : 0.5)}; + transition: opacity 0.3s linear; + color: ${p => p.theme.colors.text.main}; + font-weight: 300; + font-size: 22px; + line-height: ${p => p.theme.space[5]}px; + white-space: nowrap; + text-decoration: none; + + &:hover { + opacity: 1; + } +`; + +export const TabBorder = styled.div` + position: absolute; + bottom: -1px; + background: ${p => p.theme.colors.brand}; + height: 2px; + transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1); +`; diff --git a/web/packages/teleport/src/Integrations/IntegrationList.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx index a7eadd708a7e1..77044cf0076bc 100644 --- a/web/packages/teleport/src/Integrations/IntegrationList.tsx +++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; import styled from 'styled-components'; import { useHistory } from 'react-router'; import { Link as InternalRouteLink } from 'react-router-dom'; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx index 1f20dfa40f9d5..a18c9d5e824de 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcDashboard.tsx @@ -27,17 +27,23 @@ import { AwsResource, StatCard, } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; export function AwsOidcDashboard() { const { statsAttempt, integrationAttempt } = useAwsOidcStatus(); - if (statsAttempt.status == 'processing') { + if ( + statsAttempt.status == 'processing' || + integrationAttempt.status == 'processing' + ) { return ; } - if (statsAttempt.status == 'error') { + + if (integrationAttempt.status == 'error') { return {statsAttempt.statusText}; } - if (!statsAttempt.data) { + + if (!statsAttempt.data || !integrationAttempt.data) { return null; } @@ -45,14 +51,29 @@ export function AwsOidcDashboard() { const { awsec2, awseks, awsrds } = statsAttempt.data; const { data: integration } = integrationAttempt; return ( - - {integration && } -

Auto-Enrollment

- - - - - -
+ <> + + + {integration && } +

Auto-Enrollment

+ + + + + +
+ ); } diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx index c000977305e08..83beb1314bd92 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx @@ -18,33 +18,78 @@ import { Link as InternalLink } from 'react-router-dom'; -import { ButtonIcon, Flex, Label, Text } from 'design'; -import { ArrowLeft } from 'design/Icon'; +import { ButtonIcon, ButtonText, Flex, Text } from 'design'; +import { Plugs } from 'design/Icon'; import { HoverTooltip } from 'design/Tooltip'; import cfg from 'teleport/config'; -import { getStatusAndLabel } from 'teleport/Integrations/helpers'; import { Integration } from 'teleport/services/integrations'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { useHistory } from 'react-router'; + +export function AwsOidcHeader({ + integration, + resource = undefined, +}: { + integration: Integration; + resource?: AwsResource; +}) { + const history = useHistory(); + const divider = ( + + / + + ); -export function AwsOidcHeader({ integration }: { integration: Integration }) { - const { status, labelKind } = getStatusAndLabel(integration); return ( - + - + - - {integration.name} - - + {!resource ? ( + <> + {divider} + + {integration.name} + + + ) : ( + <> + {divider} + + history.push( + cfg.getIntegrationStatusRoute( + integration.kind, + integration.name + ) + ) + } + > + {integration.name} + + {divider} + + {resource.toUpperCase()} + + + )} ); } diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx index bd69b287af243..50d154d378e85 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcRoutes.tsx @@ -21,6 +21,8 @@ import cfg from 'teleport/config'; import { AwsOidcStatusProvider } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; +import { Details } from 'teleport/Integrations/status/AwsOidc/Details/Details'; + import { AwsOidcDashboard } from './AwsOidcDashboard'; export function AwsOidcRoutes() { @@ -28,7 +30,13 @@ export function AwsOidcRoutes() { + . + */ + +import { Link as InternalLink } from 'react-router-dom'; + +import { ButtonIcon, Flex, Label, Text } from 'design'; +import { ArrowLeft } from 'design/Icon'; +import { HoverTooltip } from 'design/Tooltip'; + +import cfg from 'teleport/config'; +import { getStatusAndLabel } from 'teleport/Integrations/helpers'; +import { Integration } from 'teleport/services/integrations'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; + +export function AwsOidcTitle({ + integration, + resource = undefined, +}: { + integration: Integration; + resource?: AwsResource; +}) { + const { status, labelKind } = getStatusAndLabel(integration); + + const content = { + to: !resource + ? cfg.routes.integrations + : cfg.getIntegrationStatusRoute(integration.kind, integration.name), + helper: !resource ? 'Back to integrations' : 'Back to integration', + content: !resource ? integration.name : resource.toUpperCase(), + }; + + return ( + + + + + + + + {content.content} + + + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx new file mode 100644 index 0000000000000..dae7b472525e1 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Agents.tsx @@ -0,0 +1,60 @@ +/** + * 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 Table, { LabelCell } from 'design/DataTable'; + +export function Agents() { + return ( + { + const aStr = a.tags.toString(); + const bStr = b.tags.toString(); + + if (aStr < bStr) { + return -1; + } + if (aStr > bStr) { + return 1; + } + + return 0; + }, + render: ({ tags }) => , + }, + ]} + emptyText="Agents details coming soon" + isSearchable + /> + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx new file mode 100644 index 0000000000000..02d185412509e --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Details.tsx @@ -0,0 +1,54 @@ +/** + * 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 { useParams } from 'react-router'; + +import { FeatureBox } from 'teleport/components/Layout'; +import { AwsOidcHeader } from 'teleport/Integrations/status/AwsOidc/AwsOidcHeader'; +import { useAwsOidcStatus } from 'teleport/Integrations/status/AwsOidc/useAwsOidcStatus'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { IntegrationKind } from 'teleport/services/integrations'; +import { Rds } from 'teleport/Integrations/status/AwsOidc/Details/Rds'; +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; +import { AwsOidcTitle } from 'teleport/Integrations/status/AwsOidc/AwsOidcTitle'; + +export function Details() { + const { resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const { integrationAttempt } = useAwsOidcStatus(); + const { data: integration } = integrationAttempt; + return ( + <> + {integration && ( + + )} + + {integration && ( + + )} + {resourceKind == AwsResource.ec2 && } + {resourceKind == AwsResource.eks && } + {resourceKind == AwsResource.rds && } + + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx new file mode 100644 index 0000000000000..bb4d8f6973bda --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rds.tsx @@ -0,0 +1,101 @@ +/** + * 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 { useEffect, useRef } from 'react'; + +import { TabBorder, TabContainer, TabsContainer } from 'design/Tabs/Tabs'; +import { useLocation, useParams } from 'react-router'; + +import { IntegrationKind } from 'teleport/services/integrations'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import cfg from 'teleport/config'; +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; +import { Agents } from 'teleport/Integrations/status/AwsOidc/Details/Agents'; + +export enum RdsTab { + Agents = 'agents', + Rules = 'rules', +} + +export function Rds() { + const { type, name, resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const { search } = useLocation(); + const searchParams = new URLSearchParams(search); + const tab = (searchParams.get('tab') as RdsTab) || RdsTab.Rules; + + const borderRef = useRef(null); + const parentRef = useRef(); + + useEffect(() => { + if (!parentRef.current || !borderRef.current) { + return; + } + + const activeElement = parentRef.current.querySelector( + `[data-tab-id="${tab}"]` + ); + + if (activeElement) { + const parentBounds = parentRef.current.getBoundingClientRect(); + const activeBounds = activeElement.getBoundingClientRect(); + + const left = activeBounds.left - parentBounds.left; + const width = activeBounds.width; + + borderRef.current.style.left = `${left}px`; + borderRef.current.style.width = `${width}px`; + } + }, [tab]); + + return ( + <> + + + Enrollment Rules + + + Agents + + + + {tab == RdsTab.Rules && } + {tab == RdsTab.Agents && } + + ); +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx new file mode 100644 index 0000000000000..0a529848c31b9 --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.test.tsx @@ -0,0 +1,110 @@ +/** + * 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, waitFor } from 'design/utils/testing'; + +import { MemoryRouter } from 'react-router'; + +import { within } from '@testing-library/react'; + +import { Rules } from 'teleport/Integrations/status/AwsOidc/Details/Rules'; +import { + IntegrationDiscoveryRule, + integrationService, +} from 'teleport/services/integrations'; + +test('renders region & labels from response', async () => { + jest.spyOn(integrationService, 'fetchIntegrationRules').mockResolvedValue({ + rules: [ + makeIntegrationDiscoveryRule({ + region: 'us-west-2', + labelMatcher: [ + { name: 'env', value: 'prod' }, + { name: 'key', value: '123' }, + ], + }), + makeIntegrationDiscoveryRule({ + region: 'us-east-2', + labelMatcher: [{ name: 'env', value: 'stage' }], + }), + makeIntegrationDiscoveryRule({ + region: 'us-west-1', + labelMatcher: [{ name: 'env', value: 'test' }], + }), + makeIntegrationDiscoveryRule({ + region: 'us-east-1', + labelMatcher: [{ name: 'env', value: 'dev' }], + }), + ], + nextKey: '', + }); + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('env:prod')).toBeInTheDocument(); + }); + + expect(getTableCellContents()).toEqual({ + header: ['Region', 'Labels'], + rows: [ + ['us-west-2', 'env:prodkey:123'], + ['us-east-2', 'env:stage'], + ['us-west-1', 'env:test'], + ['us-east-1', 'env:dev'], + ], + }); + + jest.clearAllMocks(); +}); + +function makeIntegrationDiscoveryRule( + overrides: Partial = {} +): IntegrationDiscoveryRule { + return Object.assign( + { + resourceType: '', + region: '', + labelMatcher: [], + discoveryConfig: '', + lastSync: 0, + }, + overrides + ); +} + +function getTableCellContents() { + const [header, ...rows] = screen.getAllByRole('row'); + return { + header: within(header) + .getAllByRole('columnheader') + .map(cell => cell.textContent), + rows: rows.map(row => + within(row) + .getAllByRole('cell') + .map(cell => cell.textContent) + ), + }; +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx new file mode 100644 index 0000000000000..2b0539f22f6bd --- /dev/null +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/Details/Rules.tsx @@ -0,0 +1,129 @@ +/** + * 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 { useEffect, useState } from 'react'; + +import Table, { LabelCell } from 'design/DataTable'; + +import { useParams } from 'react-router'; + +import { + IntegrationDiscoveryRule, + IntegrationKind, + integrationService, +} from 'teleport/services/integrations'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; +import { SearchPanel } from 'shared/components/Search'; +import { useServerSidePagination } from 'teleport/components/hooks'; +import { SortType } from 'design/DataTable/types'; + +export function Rules() { + const { name, resourceKind } = useParams<{ + type: IntegrationKind; + name: string; + resourceKind: AwsResource; + }>(); + + const [search, setSearch] = useState(''); + const [sort, setSort] = useState({ + fieldName: 'region', + dir: 'ASC', + }); + const serverSidePagination = + useServerSidePagination({ + pageSize: 20, + fetchFunc: async (_, params) => { + const { rules, nextKey } = + await integrationService.fetchIntegrationRules( + name, + resourceKind, + params + ); + return { agents: rules, nextKey }; + }, + clusterId: '', + params: { search, sort }, + }); + + useEffect(() => { + serverSidePagination.fetch(); + }, [search, sort]); + + return ( + + data={serverSidePagination.fetchedData.agents || undefined} + columns={[ + { + key: 'region', + headerText: 'Region', + isSortable: true, + }, + { + key: 'labelMatcher', + headerText: getResourceTerm(resourceKind), + isSortable: true, + onSort: (a, b) => { + const aStr = a.labelMatcher.toString(); + const bStr = b.labelMatcher.toString(); + + if (aStr < bStr) { + return -1; + } + if (aStr > bStr) { + return 1; + } + + return 0; + }, + render: ({ labelMatcher }) => ( + `${l.name}:${l.value}`)} /> + ), + }, + ]} + emptyText={`No ${resourceKind} data`} + isSearchable + fetching={{ + fetchStatus: serverSidePagination.fetchStatus, + onFetchNext: serverSidePagination.fetchNext, + onFetchPrev: serverSidePagination.fetchPrev, + }} + serversideProps={{ + sort: sort, + setSort: setSort, + serversideSearchPanel: ( + + ), + }} + /> + ); +} + +function getResourceTerm(resource: AwsResource): string { + switch (resource) { + case AwsResource.rds: + return 'Tags'; + default: + return 'Labels'; + } +} diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx index d219077bb81aa..d967a7ebe1f33 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/StatCard.tsx @@ -23,7 +23,15 @@ import * as Icons from 'design/Icon'; import { formatDistanceStrict } from 'date-fns'; -import { ResourceTypeSummary } from 'teleport/services/integrations'; +import styled from 'styled-components'; + +import history from 'teleport/services/history'; + +import { + IntegrationKind, + ResourceTypeSummary, +} from 'teleport/services/integrations'; +import cfg from 'teleport/config'; export enum AwsResource { ec2 = 'ec2', @@ -32,22 +40,29 @@ export enum AwsResource { } type StatCardProps = { + name: string; resource: AwsResource; summary?: ResourceTypeSummary; }; -export function StatCard({ resource, summary }: StatCardProps) { +export function StatCard({ name, resource, summary }: StatCardProps) { const updated = summary?.discoverLastSync ? new Date(summary?.discoverLastSync) : undefined; const term = getResourceTerm(resource); return ( - { + history.push( + cfg.getIntegrationStatusResourcesRoute( + IntegrationKind.AwsOidc, + name, + resource + ) + ); + }} > )} - + ); } @@ -111,3 +126,17 @@ function getResourceTerm(resource: AwsResource): string { return 'Instances'; } } + +export const SelectCard = styled(Card)` + width: 33%; + background-color: ${props => props.theme.colors.levels.surface}; + padding: 12px; + border-radius: ${props => props.theme.radii[2]}px; + border: ${props => `1px solid ${props.theme.colors.levels.surface}`}; + cursor: pointer; + + &:hover { + background-color: ${props => props.theme.colors.levels.elevated}; + box-shadow: ${({ theme }) => theme.boxShadow[2]}; + } +`; diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx index 4d29af57d0422..d667ccf0bb258 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/testHelpers/mockAwsOidcStatusProvider.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from 'react'; - import { MemoryRouter } from 'react-router'; import { diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx index f517f785595d9..c49d97d7ba577 100644 --- a/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx +++ b/web/packages/teleport/src/Integrations/status/AwsOidc/useAwsOidcStatus.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { createContext, useContext, useEffect } from 'react'; +import { createContext, useContext, useEffect } from 'react'; import { useParams } from 'react-router'; import { Attempt, useAsync } from 'shared/hooks/useAsync'; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index f475fa868e8ac..bc08ea147e425 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -26,6 +26,7 @@ import { PluginKind, Regions, } from 'teleport/services/integrations'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; import { defaultEntitlements } from './entitlement'; @@ -198,6 +199,8 @@ const cfg = { headlessSso: `/web/headless/:requestId`, integrations: '/web/integrations', integrationStatus: '/web/integrations/status/:type/:name', + integrationStatusResources: + '/web/integrations/status/:type/:name/resources/:resourceKind', integrationEnroll: '/web/integrations/new/:type?', locks: '/web/locks', newLock: '/web/locks/new', @@ -329,6 +332,9 @@ const cfg = { integrationsPath: '/v1/webapi/sites/:clusterId/integrations/:name?', integrationStatsPath: '/v1/webapi/sites/:clusterId/integrations/:name/stats', + integrationRulesPath: + '/v1/webapi/sites/:clusterId/integrations/:name/discoveryrules?resourceType=:resourceType?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?&limit=:limit?', + thumbprintPath: '/v1/webapi/thumbprint', pingAwsOidcIntegrationPath: '/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/ping', @@ -544,6 +550,18 @@ const cfg = { return generatePath(cfg.routes.integrationStatus, { type, name }); }, + getIntegrationStatusResourcesRoute( + type: PluginKind | IntegrationKind, + name: string, + resourceKind: AwsResource + ) { + return generatePath(cfg.routes.integrationStatusResources, { + type, + name, + resourceKind, + }); + }, + getMsTeamsAppZipRoute(clusterId: string, plugin: string) { return generatePath(cfg.api.msTeamsAppZipPath, { clusterId, plugin }); }, @@ -994,6 +1012,20 @@ const cfg = { }); }, + getIntegrationRulesUrl( + name: string, + resourceType: AwsResource, + params?: UrlResourcesParams + ) { + const clusterId = cfg.proxyCluster; + return generateResourcePath(cfg.api.integrationRulesPath, { + clusterId, + name, + resourceType, + ...params, + }); + }, + getPingAwsOidcIntegrationUrl({ integrationName, clusterId, diff --git a/web/packages/teleport/src/services/integrations/integrations.test.ts b/web/packages/teleport/src/services/integrations/integrations.test.ts index fb917c96a6b97..a799dd90246c8 100644 --- a/web/packages/teleport/src/services/integrations/integrations.test.ts +++ b/web/packages/teleport/src/services/integrations/integrations.test.ts @@ -19,8 +19,10 @@ import api from 'teleport/services/api'; import cfg from 'teleport/config'; +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; + import { integrationService } from './integrations'; -import { IntegrationStatusCode, IntegrationAudience } from './types'; +import { IntegrationAudience, IntegrationStatusCode } from './types'; test('fetch a single integration: fetchIntegration()', async () => { // test a valid response @@ -226,6 +228,54 @@ describe('fetchAwsDatabases() request body formatting', () => { ); }); +test('fetch integration rules: fetchIntegrationRules()', async () => { + // test a valid response + jest.spyOn(api, 'get').mockResolvedValue({ + rules: [ + { + resourceType: 'eks', + region: 'us-west-2', + labelMatcher: [{ name: 'env', value: 'dev' }], + discoveryConfig: 'cfg', + lastSync: 1733782634, + }, + ], + nextKey: 'some-key', + }); + + let response = await integrationService.fetchIntegrationRules( + 'name', + AwsResource.eks + ); + expect(api.get).toHaveBeenCalledWith( + cfg.getIntegrationRulesUrl('name', AwsResource.eks) + ); + expect(response).toEqual({ + nextKey: 'some-key', + rules: [ + { + resourceType: 'eks', + region: 'us-west-2', + labelMatcher: [{ name: 'env', value: 'dev' }], + discoveryConfig: 'cfg', + lastSync: 1733782634, + }, + ], + }); + + // test null response + jest.spyOn(api, 'get').mockResolvedValue(null); + + response = await integrationService.fetchIntegrationRules( + 'name', + AwsResource.eks + ); + expect(response).toEqual({ + nextKey: undefined, + rules: [], + }); +}); + const nonAwsOidcIntegration = { name: 'non-aws-oidc-integration', subKind: 'abc', diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index 48711112f39b8..f7fb6db4f4b2b 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -17,7 +17,9 @@ */ import api from 'teleport/services/api'; -import cfg from 'teleport/config'; +import cfg, { UrlResourcesParams } from 'teleport/config'; + +import { AwsResource } from 'teleport/Integrations/status/AwsOidc/StatCard'; import makeNode from '../nodes/makeNode'; import auth, { MfaChallengeScope } from '../auth/auth'; @@ -60,6 +62,7 @@ import { AwsOidcPingResponse, AwsOidcPingRequest, IntegrationWithSummary, + IntegrationDiscoveryRules, } from './types'; export const integrationService = { @@ -428,6 +431,21 @@ export const integrationService = { return resp; }); }, + + fetchIntegrationRules( + name: string, + resourceType: AwsResource, + params?: UrlResourcesParams + ): Promise { + return api + .get(cfg.getIntegrationRulesUrl(name, resourceType, params)) + .then(resp => { + return { + rules: resp?.rules || [], + nextKey: resp?.nextKey, + }; + }); + }, }; export function makeIntegrations(json: any): Integration[] { diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index ff8f4347b985c..d63cac5c85a96 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -298,6 +298,31 @@ export type IntegrationWithSummary = { awseks: ResourceTypeSummary; }; +// IntegrationDiscoveryRules contains the list of discovery rules for a given Integration. +export type IntegrationDiscoveryRules = { + // rules is the list of integration rules. + rules: IntegrationDiscoveryRule[]; + // nextKey is the position to resume listing rules. + nextKey: string; +}; + +// IntegrationDiscoveryRule describes a discovery rule associated with an integration. +export type IntegrationDiscoveryRule = { + // resourceType indicates the type of resource that this rule targets. + // This is the same value that is set in DiscoveryConfig.AWS..Types + // Example: ec2, rds, eks + resourceType: string; + // region where this rule applies to. + region: string; + // labelMatcher is the set of labels that are used to filter the resources before trying to auto-enroll them. + labelMatcher: Label[]; + // discoveryConfig is the name of the DiscoveryConfig that created this rule. + discoveryConfig: string; + // lastSync contains the time when this rule was used. + // If empty, it indicates that the rule is not being used. + lastSync: number; +}; + // ResourceTypeSummary contains the summary of the enrollment rules and found resources by the integration. export type ResourceTypeSummary = { // rulesCount is the number of enrollment rules that are using this integration.