diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index efc1cb865..b9f47cb7e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -635,6 +635,7 @@ export const registerRoutes = async ( }); const superAdminService = superAdminServiceFactory({ userDAL, + identityDAL, userAliasDAL, authService: loginService, serverCfgDAL: superAdminDAL, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index b63550d2c..a1c433650 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,7 +1,7 @@ import DOMPurify from "isomorphic-dompurify"; import { z } from "zod"; -import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; +import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -154,6 +154,43 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/identity-management/identities", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + searchTerm: z.string().default(""), + offset: z.coerce.number().default(0), + limit: z.coerce.number().max(100).default(20) + }), + response: { + 200: z.object({ + identities: IdentitiesSchema.pick({ + name: true, + id: true + }).array() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const identities = await server.services.superAdmin.getIdentities({ + ...req.query + }); + + return { + identities + }; + } + }); + server.route({ method: "GET", url: "/integrations/slack/config", diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts index a74a84ce3..8b7fccab3 100644 --- a/backend/src/services/identity/identity-dal.ts +++ b/backend/src/services/identity/identity-dal.ts @@ -1,10 +1,42 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { TableName, TIdentities } from "@app/db/schemas"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { DatabaseError } from "@app/lib/errors"; export type TIdentityDALFactory = ReturnType; export const identityDALFactory = (db: TDbClient) => { const identityOrm = ormify(db, TableName.Identity); - return identityOrm; + + const getIdentitiesByFilter = async ({ + limit, + offset, + searchTerm, + sortBy + }: { + limit: number; + offset: number; + searchTerm: string; + sortBy?: keyof TIdentities; + }) => { + try { + let query = db.replicaNode()(TableName.Identity); + + if (searchTerm) { + query = query.where((qb) => { + void qb.whereILike("name", `%${searchTerm}%`); + }); + } + + if (sortBy) { + query = query.orderBy(sortBy); + } + + return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity)); + } catch (error) { + throw new DatabaseError({ error, name: "Get identities by filter" }); + } + }; + + return { ...identityOrm, getIdentitiesByFilter }; }; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8efdfd7f9..e8e01a44f 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -19,9 +19,11 @@ import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; -import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; +import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; type TSuperAdminServiceFactoryDep = { + identityDAL: Pick; serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; userAliasDAL: Pick; @@ -51,6 +53,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, userDAL, + identityDAL, userAliasDAL, authService, orgService, @@ -143,8 +146,8 @@ export const superAdminServiceFactory = ({ const canServerAdminAccessAfterApply = data.enabledLoginMethods.some((loginMethod) => - loginMethodToAuthMethod[loginMethod as LoginMethod].some( - (authMethod) => superAdminUser.authMethods?.includes(authMethod) + loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) => + superAdminUser.authMethods?.includes(authMethod) ) ) || isUserSamlAccessEnabled || @@ -292,6 +295,15 @@ export const superAdminServiceFactory = ({ return user; }; + const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { + return identityDAL.getIdentitiesByFilter({ + limit, + offset, + searchTerm, + sortBy: "name" + }); + }; + const grantServerAdminAccessToUser = async (userId: string) => { if (!licenseService.onPremFeatures?.instanceUserManagement) { throw new BadRequestError({ @@ -389,6 +401,7 @@ export const superAdminServiceFactory = ({ adminSignUp, getUsers, deleteUser, + getIdentities, getAdminSlackConfig, updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index d6de67e59..54a42c2ca 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -23,6 +23,12 @@ export type TAdminGetUsersDTO = { adminsOnly: boolean; }; +export type TAdminGetIdentitiesDTO = { + offset: number; + limit: number; + searchTerm: string; +}; + export enum LoginMethod { EMAIL = "email", GOOGLE = "google", diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 12ae1cf64..7e2d60831 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -4,19 +4,24 @@ import { apiRequest } from "@app/config/request"; import { User } from "../types"; import { + AdminGetIdentitiesFilters, AdminGetUsersFilters, AdminSlackConfig, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; +import { Identity } from "@app/hooks/api/identities/types.ts"; export const adminStandaloneKeys = { - getUsers: "get-users" + getUsers: "get-users", + getIdentities: "get-identities" }; export const adminQueryKeys = { serverConfig: () => ["server-config"] as const, getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, + getIdentities: (filters: AdminGetIdentitiesFilters) => + [adminStandaloneKeys.getIdentities, { filters }] as const, getAdminSlackConfig: () => ["admin-slack-config"] as const, getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const }; @@ -68,6 +73,28 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => { }); }; +export const useAdminGetIdentities = (filters: AdminGetIdentitiesFilters) => { + return useInfiniteQuery({ + initialPageParam: 0, + queryKey: adminQueryKeys.getIdentities(filters), + queryFn: async ({ pageParam }) => { + const { data } = await apiRequest.get<{ identities: Identity[] }>( + "/api/v1/admin/identity-management/identities", + { + params: { + ...filters, + offset: pageParam + } + } + ); + + return data.identities; + }, + getNextPageParam: (lastPage, pages) => + lastPage.length !== 0 ? pages.length * filters.limit : undefined + }); +}; + export const useGetAdminSlackConfig = () => { return useQuery({ queryKey: adminQueryKeys.getAdminSlackConfig(), diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 80e3edc92..11f2cf44f 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -53,6 +53,11 @@ export type AdminGetUsersFilters = { adminsOnly: boolean; }; +export type AdminGetIdentitiesFilters = { + limit: number; + searchTerm: string; +}; + export type AdminSlackConfig = { clientId: string; clientSecret: string; diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index ad07b71ce..f48665653 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -34,6 +34,7 @@ import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; import { RateLimitPanel } from "./components/RateLimitPanel"; import { UserPanel } from "./components/UserPanel"; +import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel.tsx"; enum TabSections { Settings = "settings", @@ -42,6 +43,7 @@ enum TabSections { RateLimit = "rate-limit", Integrations = "integrations", Users = "users", + Identities = "identities", Kmip = "kmip" } @@ -164,6 +166,7 @@ export const OverviewPage = () => { Rate Limit Integrations Users + Identities @@ -409,6 +412,9 @@ export const OverviewPage = () => { + + + )} diff --git a/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx new file mode 100644 index 000000000..4851db656 --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/IdentityPanel.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + EmptyState, + Input, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useDebounce } from "@app/hooks"; +import { useAdminGetIdentities } from "@app/hooks/api/admin/queries.ts"; + +const IdentityPanelTable = () => { + const [searchIdentityFilter, setSearchIdentityFilter] = useState(""); + const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500); + + const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities( + { + limit: 20, + searchTerm: debouncedSearchTerm + } + ); + + const isEmpty = !isPending && !data?.pages?.[0].length; + + return ( + <> +
+ setSearchIdentityFilter(e.target.value)} + leftIcon={} + placeholder="Search identities by name..." + className="flex-1" + /> +
+
+ + + + + + + + + + {isPending && } + {!isPending && + data?.pages?.map((identities) => + identities.map(({ name, authMethod, id }) => ( + + + + + )) + )} + +
NameAuth Method
{name} + {authMethod || None} +
+ {!isPending && isEmpty && } +
+ {!isEmpty && ( + + )} +
+ + ); +}; + +export const IdentityPanel = () => ( +
+
+

Identities

+
+ +
+); diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 6654439de..0e5173188 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -61,12 +61,12 @@ const UserPanelTable = ({ const [adminsOnly, setAdminsOnly] = useState(false); const { user } = useUser(); const userId = user?.id || ""; - const [debounedSearchTerm] = useDebounce(searchUserFilter, 500); + const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); const { subscription } = useSubscription(); const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({ limit: 20, - searchTerm: debounedSearchTerm, + searchTerm: debouncedSearchTerm, adminsOnly });