Skip to content

Commit

Permalink
Add teleterm support for access requesting kube namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
kimlisa committed Oct 8, 2024
1 parent e2dcd18 commit 357f75a
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 28 deletions.
14 changes: 14 additions & 0 deletions web/packages/shared/components/AccessRequests/NewRequest/kube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import { KubeResourceKind } from 'teleport/services/kube';

import { PendingListItem } from './RequestCheckout';
import { RequestableResourceKind } from './resource';

export type KubeNamespaceRequest = {
kubeCluster: string;
Expand Down Expand Up @@ -78,3 +79,16 @@ export function getKubeResourceRequestMode(
disableCheckoutFromKubeRestrictions,
};
}

export function requiresKubeResourceSelection({
dryRun,
requestMode,
kind,
}: {
dryRun: boolean;
requestMode: KubeResourceKind[];
kind: RequestableResourceKind;
}) {
const requiresKubeResourceSelection = requestMode.length > 0;
return dryRun && kind === 'kube_cluster' && requiresKubeResourceSelection;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import * as Icon from 'design/Icon';
import { pluralize } from 'shared/utils/text';

import { RequestCheckoutWithSlider } from 'shared/components/AccessRequests/NewRequest';
import { excludeKubeClusterWithNamespaces } from 'shared/components/AccessRequests/NewRequest/kube';

import useAccessRequestCheckout from './useAccessRequestCheckout';
import { AssumedRolesBar } from './AssumedRolesBar';
Expand Down Expand Up @@ -102,6 +103,9 @@ export function AccessRequestCheckout() {
pendingRequestTtlOptions,
startTime,
onStartTimeChange,
fetchKubeNamespaces,
bulkToggleKubeResources,
allowedKubeSubresourceKinds,
} = useAccessRequestCheckout();

const isRoleRequest = data[0]?.kind === 'role';
Expand All @@ -110,12 +114,21 @@ export function AccessRequestCheckout() {
setShowCheckout(false);
}

const filteredData = data?.filter(d =>
excludeKubeClusterWithNamespaces(d, data)
);

const numAddedResources = filteredData?.length;

// We should rather detect how much space we have,
// but for simplicity we only count items.
const moreToShow = Math.max(data.length - MAX_RESOURCES_IN_BAR_TO_SHOW, 0);
const moreToShow = Math.max(
filteredData.length - MAX_RESOURCES_IN_BAR_TO_SHOW,
0
);
return (
<>
{data.length > 0 && !isCollapsed() && (
{filteredData.length > 0 && !isCollapsed() && (
<Box
px={3}
py={2}
Expand All @@ -133,17 +146,22 @@ export function AccessRequestCheckout() {
>
<Flex flexDirection="column" minWidth={0}>
<Text mb={1}>
{data.length}{' '}
{pluralize(data.length, isRoleRequest ? 'role' : 'resource')}{' '}
{numAddedResources}{' '}
{pluralize(
numAddedResources,
isRoleRequest ? 'role' : 'resource'
)}{' '}
added to access request:
</Text>
<Flex gap={1} flexWrap="wrap">
{data
{filteredData
.slice(0, MAX_RESOURCES_IN_BAR_TO_SHOW)
.map(c => {
let resource = {
name: c.name,
key: `${c.clusterName}-${c.kind}-${c.id}`,
name: c.subResourceName
? `${c.id}/${c.subResourceName}`
: c.name,
key: `${c.clusterName}-${c.kind}-${c.id}-${c.subResourceName}`,
Icon: undefined,
};
switch (c.kind) {
Expand All @@ -158,6 +176,7 @@ export function AccessRequestCheckout() {
resource.Icon = Icon.Database;
break;
case 'kube_cluster':
case 'namespace':
resource.Icon = Icon.Kubernetes;
break;
case 'role':
Expand Down Expand Up @@ -259,6 +278,9 @@ export function AccessRequestCheckout() {
setPendingRequestTtl={setPendingRequestTtl}
startTime={startTime}
onStartTimeChange={onStartTimeChange}
fetchKubeNamespaces={fetchKubeNamespaces}
bulkToggleKubeResources={bulkToggleKubeResources}
allowedKubeSubresourceKinds={allowedKubeSubresourceKinds}
/>
)}
</Transition>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useState, useEffect } from 'react';
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';

import useAttempt from 'shared/hooks/useAttemptNext';
import { Option } from 'shared/components/Select';

import {
getDryRunMaxDuration,
Expand All @@ -28,12 +29,20 @@ import {
import { useSpecifiableFields } from 'shared/components/AccessRequests/NewRequest/useSpecifiableFields';

import { CreateRequest } from 'shared/components/AccessRequests/Shared/types';
import {
excludeKubeClusterWithNamespaces,
KubeNamespaceRequest,
requiresKubeResourceSelection,
} from 'shared/components/AccessRequests/NewRequest/kube';
import { PendingKubeResourceItem } from 'shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout';
import { KubeResourceKind } from 'teleport/services/kube';

import { useAppContext } from 'teleterm/ui/appContextProvider';
import {
PendingAccessRequest,
extractResourceRequestProperties,
ResourceRequest,
toResourceRequest,
} from 'teleterm/ui/services/workspacesService/accessRequestsService';
import { retryWithRelogin } from 'teleterm/ui/utils';
import {
Expand All @@ -55,6 +64,13 @@ export default function useAccessRequestCheckout() {
ctx.workspacesService?.getActiveWorkspace()?.localClusterUri;
const rootClusterUri = ctx.workspacesService?.getRootClusterUri();

const loggedInUser =
ctx.clustersService.findCluster(rootClusterUri)?.loggedInUser;
const allowedKubeSubresourceKinds =
loggedInUser?.requestMode?.kubernetesResources?.map(
r => r.kind as KubeResourceKind
) || [];

const {
selectedReviewers,
setSelectedReviewers,
Expand Down Expand Up @@ -120,7 +136,7 @@ export default function useAccessRequestCheckout() {
name: d.id,
kind: d.kind,
clusterName: d.clusterName,
subResourceName: '',
subResourceName: d.subResourceName || '',
})),
});
setResourceRequestRoles(response.applicableRoles);
Expand Down Expand Up @@ -171,15 +187,20 @@ export default function useAccessRequestCheckout() {
pendingRequest.resources.forEach(resourceRequest => {
const { kind, id, name } =
extractResourceRequestProperties(resourceRequest);
data.push({
const item: PendingListItemWithOriginalItem = {
kind,
id,
name,
originalItem: resourceRequest,
clusterName: ctx.clustersService.findClusterByResource(
resourceRequest.resource.uri
)?.name,
});
};

if (kind === 'namespace') {
item.subResourceName = name;
}
data.push(item);
});
}
}
Expand All @@ -204,6 +225,58 @@ export default function useAccessRequestCheckout() {
await workspaceAccessRequest.addOrRemoveResource(
pendingListItem.originalItem
);

if (pendingListItem.kind === 'kube_cluster') {
deleteKubeClustersNamespaces({
kubeClusterUri: pendingListItem.originalItem.resource.uri,
kubeClusterId: pendingListItem.id,
});
}
}

async function deleteKubeClustersNamespaces({
kubeClusterUri,
kubeClusterId,
}: {
kubeClusterUri: string;
kubeClusterId: string;
}) {
const pending = workspaceAccessRequest.getPendingAccessRequest();
if (pending.kind === 'role') return;
const hasInsertedItem = pending.resources.has(kubeClusterUri);

if (!hasInsertedItem) {
const namespacesToDelete: ResourceRequest[] = [];
pending.resources.forEach(value => {
if (value.kind === 'namespace') {
const { kubeId } = routing.parseKubeResourceNamespaceUri(
value.resource.uri
).params;
if (kubeId === kubeClusterId) {
namespacesToDelete.push(value);
}
}
});
if (namespacesToDelete.length) {
await workspaceAccessRequest.addOrRemoveResources(namespacesToDelete);
}
}
}

async function bulkToggleKubeResources(
items: PendingKubeResourceItem[],
kubeCluster: PendingListKubeClusterWithOriginalItem
) {
await workspaceAccessRequest.addOrRemoveResources(
items.map(item => {
return toResourceRequest({
kind: item.kind,
resourceId: item.id,
resourceName: item.subResourceName,
clusterUri: kubeCluster.originalItem.resource.uri,
});
})
);
}

function getAssumedRequests() {
Expand All @@ -222,25 +295,53 @@ export default function useAccessRequestCheckout() {
*/
function prepareAndCreateRequest(req: CreateRequest) {
const data = getPendingAccessRequestsPerResource(pendingAccessRequest);

const params: CreateAccessRequestRequest = {
rootClusterUri,
reason: req.reason,
suggestedReviewers: req.suggestedReviewers || [],
dryRun: req.dryRun,
resourceIds: data
.filter(d => d.kind !== 'role')
.map(d => ({
name: d.id,
clusterName: d.clusterName,
kind: d.kind,
subResourceName: '',
})),
.filter(d => excludeKubeClusterWithNamespaces(d, data))
// Skip dry running with kube_cluster that requires
// subresource selection. Otherwise the user will see
// an error saying they can't make kube_cluster requests.
.filter(
d =>
!requiresKubeResourceSelection({
dryRun: req.dryRun,
kind: d.kind,
requestMode: allowedKubeSubresourceKinds,
})
)
.map(d => {
if (d.kind === 'namespace') {
return {
name: d.id,
kind: d.kind,
clusterName: d.clusterName,
subResourceName: d.subResourceName,
};
}
return {
name: d.id,
clusterName: d.clusterName,
kind: d.kind,
subResourceName: '',
};
}),
roles: data.filter(d => d.kind === 'role').map(d => d.name),
assumeStartTime: req.start && Timestamp.fromDate(req.start),
maxDuration: req.maxDuration && Timestamp.fromDate(req.maxDuration),
requestTtl: req.requestTTL && Timestamp.fromDate(req.requestTTL),
};

// Don't attempt creating anything if there are no resources selected.
if (!params.resourceIds.length && !params.roles.length) {
return;
}

// if we have a resource access request, we pass along the selected roles from the checkout
if (params.resourceIds.length > 0) {
params.roles = selectedResourceRequestRoles;
Expand All @@ -250,7 +351,12 @@ export default function useAccessRequestCheckout() {

return retryWithRelogin(ctx, clusterUri, () =>
ctx.clustersService.createAccessRequest(params).then(({ response }) => {
return { accessRequest: response.request, requestedCount: data.length };
return {
accessRequest: response.request,
requestedCount: data.filter(d =>
excludeKubeClusterWithNamespaces(d, data)
).length,
};
})
).catch(e => {
setCreateRequestAttempt({ status: 'failed', statusText: e.message });
Expand Down Expand Up @@ -326,6 +432,30 @@ export default function useAccessRequestCheckout() {
}
}

async function fetchKubeNamespaces({
kubeCluster,
search,
}: KubeNamespaceRequest): Promise<Option[]> {
const { response } = await ctx.tshd.listKubernetesResources({
searchKeywords: search,
limit: 50,
useSearchAsRoles: true,
nextKey: '',
resourceType: 'namespace',
clusterUri,
predicateExpression: '',
kubernetesCluster: kubeCluster,
kubernetesNamespace: '',
});
return response.resources.map(i => {
return {
kind: 'namespace',
value: i.name,
label: i.name,
};
});
}

const shouldShowClusterNameColumn =
pendingAccessRequest?.kind === 'resource' &&
Array.from(pendingAccessRequest.resources.values()).some(a =>
Expand Down Expand Up @@ -365,6 +495,9 @@ export default function useAccessRequestCheckout() {
pendingRequestTtlOptions,
startTime,
onStartTimeChange,
fetchKubeNamespaces,
bulkToggleKubeResources,
allowedKubeSubresourceKinds,
};
}

Expand All @@ -378,3 +511,8 @@ type PendingListItemWithOriginalItem = Omit<PendingListItem, 'kind'> &
kind: 'role';
}
);

type PendingListKubeClusterWithOriginalItem = Omit<PendingListItem, 'kind'> & {
kind: Extract<ResourceKind, 'kube_cluster'>;
originalItem: ResourceRequest;
};
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ function toResourceMap(request: PendingAccessRequest): ResourceMap {
db: {},
app: {},
saml_idp_service_provider: {},
namespace: {},
};
if (request.kind === 'role') {
request.roles.forEach(role => {
Expand Down
Loading

0 comments on commit 357f75a

Please sign in to comment.