Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ func (h *Handler) bindDefaultEndpoints() {
// User Status (used by client to check if user session is valid)
h.GET("/webapi/user/status", h.WithAuth(h.getUserStatus))

h.GET("/webapi/roles", h.WithAuth(h.getRolesHandle))
h.GET("/webapi/roles", h.WithAuth(h.listRolesHandle))
h.POST("/webapi/roles", h.WithAuth(h.upsertRoleHandle))
h.PUT("/webapi/roles/:name", h.WithAuth(h.upsertRoleHandle))
h.DELETE("/webapi/roles/:name", h.WithAuth(h.deleteRole))
Expand Down
51 changes: 50 additions & 1 deletion lib/web/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,19 @@ func (h *Handler) checkAccessToRegisteredResource(w http.ResponseWriter, r *http
}, nil
}

func (h *Handler) getRolesHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
func (h *Handler) listRolesHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
clt, err := ctx.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}

values := r.URL.Query()
// If limit exists as a query parameter, this means its coming from a "new" webui
// and can return the new paginated response.
// TODO(gzdunek): DELETE IN 17.0.0: remove "getRoles".
if values.Has("limit") {
return listRoles(clt, values)
}
return getRoles(clt)
}

Expand All @@ -93,6 +100,39 @@ func getRoles(clt resourcesAPIGetter) ([]ui.ResourceItem, error) {
return ui.NewRoles(roles)
}

func listRoles(clt resourcesAPIGetter, values url.Values) (*listResourcesWithoutCountGetResponse, error) {
limit, err := queryLimitAsInt32(values, "limit", defaults.MaxIterationLimit)
if err != nil {
return nil, trace.Wrap(err)
}

roles, err := clt.ListRoles(context.TODO(), &proto.ListRolesRequest{
Limit: limit,
StartKey: values.Get("startKey"),
Filter: &types.RoleFilter{
SearchKeywords: client.ParseSearchKeywords(values.Get("search"), ' '),
},
})
if err != nil {
return nil, trace.Wrap(err)
}

var typeRoles []types.Role
for _, role := range roles.GetRoles() {
typeRoles = append(typeRoles, role)
}

uiRoles, err := ui.NewRoles(typeRoles)
if err != nil {
return nil, trace.Wrap(err)
}

return &listResourcesWithoutCountGetResponse{
Items: uiRoles,
StartKey: roles.GetNextKey(),
}, nil
}

func (h *Handler) deleteRole(w http.ResponseWriter, r *http.Request, params httprouter.Params, ctx *SessionContext) (interface{}, error) {
clt, err := ctx.GetClient()
if err != nil {
Expand Down Expand Up @@ -457,6 +497,13 @@ type listResourcesGetResponse struct {
TotalCount int `json:"totalCount"`
}

type listResourcesWithoutCountGetResponse struct {
// Items is a list of resources retrieved.
Items interface{} `json:"items"`
// StartKey is the position to resume search events.
StartKey string `json:"startKey"`
}

type checkAccessToRegisteredResourceResponse struct {
// HasResource is a flag to indicate if user has any access
// to a registered resource or not.
Expand All @@ -468,6 +515,8 @@ type resourcesAPIGetter interface {
GetRole(ctx context.Context, name string) (types.Role, error)
// GetRoles returns a list of roles
GetRoles(ctx context.Context) ([]types.Role, error)
// ListRoles returns a paginated list of roles.
ListRoles(ctx context.Context, req *proto.ListRolesRequest) (*proto.ListRolesResponse, error)
// UpsertRole creates or updates role
UpsertRole(ctx context.Context, role types.Role) error
// UpsertGithubConnector creates or updates a Github connector
Expand Down
21 changes: 16 additions & 5 deletions lib/web/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,22 +291,25 @@ version: v2
func TestGetRoles(t *testing.T) {
m := &mockedResourceAPIGetter{}

m.mockGetRoles = func(ctx context.Context) ([]types.Role, error) {
m.mockListRoles = func(ctx context.Context, req *proto.ListRolesRequest) (*proto.ListRolesResponse, error) {
role, err := types.NewRole("test", types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{"test"},
},
})
require.Nil(t, err)

return []types.Role{role}, nil
return &proto.ListRolesResponse{
Roles: []*types.RoleV6{role.(*types.RoleV6)},
NextKey: "",
}, nil
}

// Test response is converted to ui objects.
roles, err := getRoles(m)
roles, err := listRoles(m, url.Values{})
require.Nil(t, err)
require.Len(t, roles, 1)
require.Contains(t, roles[0].Content, "name: test")
require.Len(t, roles.Items, 1)
require.Contains(t, roles.Items.([]ui.ResourceItem)[0].Content, "name: test")
}

func TestUpsertRole(t *testing.T) {
Expand Down Expand Up @@ -640,6 +643,7 @@ func TestListResources(t *testing.T) {
type mockedResourceAPIGetter struct {
mockGetRole func(ctx context.Context, name string) (types.Role, error)
mockGetRoles func(ctx context.Context) ([]types.Role, error)
mockListRoles func(ctx context.Context, req *proto.ListRolesRequest) (*proto.ListRolesResponse, error)
mockUpsertRole func(ctx context.Context, role types.Role) error
mockUpsertGithubConnector func(ctx context.Context, connector types.GithubConnector) error
mockGetGithubConnectors func(ctx context.Context, withSecrets bool) ([]types.GithubConnector, error)
Expand All @@ -666,6 +670,13 @@ func (m *mockedResourceAPIGetter) GetRoles(ctx context.Context) ([]types.Role, e
return nil, trace.NotImplemented("mockGetRoles not implemented")
}

func (m *mockedResourceAPIGetter) ListRoles(ctx context.Context, req *proto.ListRolesRequest) (*proto.ListRolesResponse, error) {
if m.mockListRoles != nil {
return m.mockListRoles(ctx, req)
}
return nil, trace.NotImplemented("mockListRoles not implemented")
}

func (m *mockedResourceAPIGetter) UpsertRole(ctx context.Context, role types.Role) error {
if m.mockUpsertRole != nil {
return m.mockUpsertRole(ctx, role)
Expand Down
7 changes: 4 additions & 3 deletions web/packages/design/src/DataTable/Pager/ServerSidePager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { CircleArrowLeft, CircleArrowRight } from 'design/Icon';

import { StyledArrowBtn } from './StyledPager';

export function ServerSidePager({ nextPage, prevPage }: Props) {
const isNextDisabled = !nextPage;
const isPrevDisabled = !prevPage;
export function ServerSidePager({ nextPage, prevPage, isLoading }: Props) {
const isNextDisabled = !nextPage || isLoading;
const isPrevDisabled = !prevPage || isLoading;

return (
<Flex justifyContent="flex-end" width="100%">
Expand All @@ -50,6 +50,7 @@ export function ServerSidePager({ nextPage, prevPage }: Props) {
}

export type Props = {
isLoading: boolean;
nextPage: (() => void) | null;
prevPage: (() => void) | null;
};
11 changes: 9 additions & 2 deletions web/packages/design/src/DataTable/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Text, Indicator, Box, Flex } from 'design';
import * as Icons from 'design/Icon';

import { StyledTable, StyledPanel, StyledTableWrapper } from './StyledTable';
import { TableProps } from './types';
import { TableProps, FetchStatus } from './types';
import { SortHeaderCell, TextCell } from './Cells';
import { ClientSidePager, ServerSidePager } from './Pager';
import InputSearch from './InputSearch';
Expand Down Expand Up @@ -147,6 +147,7 @@ export function Table<T>({
prevPage={fetching.onFetchPrev}
pagination={state.pagination}
serversideProps={serversideProps}
fetchStatus={fetching.fetchStatus}
/>
</StyledTableWrapper>
);
Expand Down Expand Up @@ -322,6 +323,7 @@ function ServersideTable<T>({
className,
style,
serversideProps,
fetchStatus,
}: ServersideTableProps<T>) {
return (
<>
Expand All @@ -331,7 +333,11 @@ function ServersideTable<T>({
{renderBody(data)}
</StyledTable>
<StyledPanel showTopBorder={true}>
<ServerSidePager nextPage={nextPage} prevPage={prevPage} />
<ServerSidePager
nextPage={nextPage}
prevPage={prevPage}
isLoading={fetchStatus === 'loading'}
/>
</StyledPanel>
</>
);
Expand Down Expand Up @@ -438,4 +444,5 @@ type ServersideTableProps<T> = BasicTableProps<T> & {
prevPage: () => void;
pagination: State<T>['state']['pagination'];
serversideProps: State<T>['serversideProps'];
fetchStatus?: FetchStatus;
};
30 changes: 18 additions & 12 deletions web/packages/shared/components/Search/SearchPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export function SearchPanel({
filter,
showSearchBar,
disableSearch,
hideAdvancedSearch,
extraChildren,
}: {
updateQuery(s: string): void;
updateSearch(s: string): void;
pageIndicators: { from: number; to: number; total: number };
pageIndicators?: { from: number; to: number; total: number };
filter: ResourceFilter;
showSearchBar: boolean;
disableSearch: boolean;
hideAdvancedSearch?: boolean;
extraChildren?: JSX.Element;
}) {
const [query, setQuery] = useState(filter.search || filter.query || '');
Expand Down Expand Up @@ -79,22 +81,26 @@ export function SearchPanel({
>
{showSearchBar && (
<InputSearch searchValue={query} setSearchValue={setQuery}>
<AdvancedSearchToggle
isToggled={isAdvancedSearch}
onToggle={onToggle}
px={3}
/>
{!hideAdvancedSearch && (
<AdvancedSearchToggle
isToggled={isAdvancedSearch}
onToggle={onToggle}
px={3}
/>
)}
</InputSearch>
)}
</StyledFlex>
</Flex>
<Flex alignItems="center">
<PageIndicatorText
from={pageIndicators.from}
to={pageIndicators.to}
count={pageIndicators.total}
/>
{extraChildren && extraChildren}
{pageIndicators && (
<PageIndicatorText
from={pageIndicators.from}
to={pageIndicators.to}
count={pageIndicators.total}
/>
)}
{extraChildren}
</Flex>
</Flex>
</StyledPanel>
Expand Down
1 change: 1 addition & 0 deletions web/packages/teleport/src/Apps/Apps.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ export const props: State = {
replaceHistory: () => null,
isSearchEmpty: false,
onLabelClick: () => null,
modifyFetchedData: () => null,
};
1 change: 1 addition & 0 deletions web/packages/teleport/src/Databases/Databases.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,5 @@ export const props: State = {
isSearchEmpty: false,
onLabelClick: () => null,
accessRequestId: null,
modifyFetchedData: () => null,
};
1 change: 1 addition & 0 deletions web/packages/teleport/src/Desktops/Desktops.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ export const props: State = {
replaceHistory: () => null,
isSearchEmpty: false,
onLabelClick: () => null,
modifyFetchedData: () => null,
};
1 change: 1 addition & 0 deletions web/packages/teleport/src/Kubes/Kubes.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,5 @@ export const props: State = {
isSearchEmpty: false,
onLabelClick: () => null,
accessRequestId: null,
modifyFetchedData: () => null,
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@
import React from 'react';
import Table from 'design/DataTable';

import { Resource, KindRole } from 'teleport/services/resources';
import { RoleResource } from 'teleport/services/resources';

import { renderActionCell, SimpleListProps } from '../common';
import { renderActionCell, ServerSideListProps } from '../common';

export function Roles(
props: SimpleListProps & { roles: Resource<KindRole>[] }
) {
export function Roles(props: ServerSideListProps & { roles: RoleResource[] }) {
const {
roles = [],
selectedResources,
Expand All @@ -48,8 +46,7 @@ export function Roles(
},
]}
emptyText="No Roles Found"
pagination={{ pageSize: props.pageSize }}
isSearchable
disableFilter
fetching={{
fetchStatus,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,23 @@ import { Desktop } from 'teleport/services/desktops';
import { Node } from 'teleport/services/nodes';
import { useServerSidePagination } from 'teleport/components/hooks';
import useTeleport from 'teleport/useTeleport';
import cfg from 'teleport/config';
import cfg, { UrlResourcesParams } from 'teleport/config';
import Ctx from 'teleport/teleportContext';

import { RoleResource } from 'teleport/services/resources';

import { TableWrapper, ServerSideListProps } from '../common';
import { CommonListProps, LockResourceKind } from '../../common';

import { Nodes } from './Nodes';
import { Desktops } from './Desktops';
import { Roles } from './Roles';

import type { ResourceLabel, ResourceFilter } from 'teleport/services/agents';
import type {
ResourceLabel,
ResourceFilter,
ResourcesResponse,
} from 'teleport/services/agents';

export function ServerSideSupportedList(props: CommonListProps) {
const ctx = useTeleport();
Expand All @@ -56,7 +63,7 @@ export function ServerSideSupportedList(props: CommonListProps) {
fetchFunc: getFetchFuncForServerSidePaginating(
ctx,
props.selectedResourceKind
) as any,
),
clusterId: cfg.proxyCluster, // Locking only supported with root cluster
params: resourceFilter,
pageSize: props.pageSize,
Expand Down Expand Up @@ -110,6 +117,10 @@ export function ServerSideSupportedList(props: CommonListProps) {
};

switch (props.selectedResourceKind) {
case 'role':
return (
<Roles roles={fetchedData.agents as RoleResource[]} {...listProps} />
);
case 'node':
return <Nodes nodes={fetchedData.agents as Node[]} {...listProps} />;
case 'windows_desktop':
Expand Down Expand Up @@ -140,6 +151,7 @@ export function ServerSideSupportedList(props: CommonListProps) {
to: pageIndicators.to,
total: pageIndicators.totalCount,
}}
hideAdvancedSearch={props.selectedResourceKind === 'role'} // Roles don't support advanced search.
filter={resourceFilter}
showSearchBar={true}
disableSearch={fetchStatus === 'loading'}
Expand Down Expand Up @@ -182,7 +194,16 @@ function getDefaultSort(kind: LockResourceKind): SortType {
function getFetchFuncForServerSidePaginating(
ctx: Ctx,
resourceKind: LockResourceKind
) {
): (
clusterId: string,
params: UrlResourcesParams
) => Promise<ResourcesResponse<unknown>> {
if (resourceKind === 'role') {
return async (clusterId, params) => {
const { items, startKey } = await ctx.resourceService.fetchRoles(params);
return { agents: items, startKey };
};
}
if (resourceKind === 'node') {
return ctx.nodeService.fetchNodes;
}
Expand Down
Loading