mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 17:26:19 +00:00
feat(admin): add ability to view machine identities
This commit is contained in:
@@ -635,6 +635,7 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
const superAdminService = superAdminServiceFactory({
|
const superAdminService = superAdminServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
|
identityDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
authService: loginService,
|
authService: loginService,
|
||||||
serverCfgDAL: superAdminDAL,
|
serverCfgDAL: superAdminDAL,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { z } from "zod";
|
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 { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/integrations/slack/config",
|
url: "/integrations/slack/config",
|
||||||
|
|||||||
@@ -1,10 +1,42 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TIdentities } from "@app/db/schemas";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
|
||||||
export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
|
export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
|
||||||
|
|
||||||
export const identityDALFactory = (db: TDbClient) => {
|
export const identityDALFactory = (db: TDbClient) => {
|
||||||
const identityOrm = ormify(db, TableName.Identity);
|
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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ import { TUserDALFactory } from "../user/user-dal";
|
|||||||
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
import { UserAliasType } from "../user-alias/user-alias-types";
|
import { UserAliasType } from "../user-alias/user-alias-types";
|
||||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
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 = {
|
type TSuperAdminServiceFactoryDep = {
|
||||||
|
identityDAL: Pick<TIdentityDALFactory, "getIdentitiesByFilter">;
|
||||||
serverCfgDAL: TSuperAdminDALFactory;
|
serverCfgDAL: TSuperAdminDALFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
|
||||||
@@ -51,6 +53,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
|||||||
export const superAdminServiceFactory = ({
|
export const superAdminServiceFactory = ({
|
||||||
serverCfgDAL,
|
serverCfgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
identityDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
authService,
|
authService,
|
||||||
orgService,
|
orgService,
|
||||||
@@ -143,8 +146,8 @@ export const superAdminServiceFactory = ({
|
|||||||
|
|
||||||
const canServerAdminAccessAfterApply =
|
const canServerAdminAccessAfterApply =
|
||||||
data.enabledLoginMethods.some((loginMethod) =>
|
data.enabledLoginMethods.some((loginMethod) =>
|
||||||
loginMethodToAuthMethod[loginMethod as LoginMethod].some(
|
loginMethodToAuthMethod[loginMethod as LoginMethod].some((authMethod) =>
|
||||||
(authMethod) => superAdminUser.authMethods?.includes(authMethod)
|
superAdminUser.authMethods?.includes(authMethod)
|
||||||
)
|
)
|
||||||
) ||
|
) ||
|
||||||
isUserSamlAccessEnabled ||
|
isUserSamlAccessEnabled ||
|
||||||
@@ -292,6 +295,15 @@ export const superAdminServiceFactory = ({
|
|||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
|
||||||
|
return identityDAL.getIdentitiesByFilter({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchTerm,
|
||||||
|
sortBy: "name"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const grantServerAdminAccessToUser = async (userId: string) => {
|
const grantServerAdminAccessToUser = async (userId: string) => {
|
||||||
if (!licenseService.onPremFeatures?.instanceUserManagement) {
|
if (!licenseService.onPremFeatures?.instanceUserManagement) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -389,6 +401,7 @@ export const superAdminServiceFactory = ({
|
|||||||
adminSignUp,
|
adminSignUp,
|
||||||
getUsers,
|
getUsers,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
getIdentities,
|
||||||
getAdminSlackConfig,
|
getAdminSlackConfig,
|
||||||
updateRootEncryptionStrategy,
|
updateRootEncryptionStrategy,
|
||||||
getConfiguredEncryptionStrategies,
|
getConfiguredEncryptionStrategies,
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ export type TAdminGetUsersDTO = {
|
|||||||
adminsOnly: boolean;
|
adminsOnly: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAdminGetIdentitiesDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
searchTerm: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum LoginMethod {
|
export enum LoginMethod {
|
||||||
EMAIL = "email",
|
EMAIL = "email",
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
|
|||||||
@@ -4,19 +4,24 @@ import { apiRequest } from "@app/config/request";
|
|||||||
|
|
||||||
import { User } from "../types";
|
import { User } from "../types";
|
||||||
import {
|
import {
|
||||||
|
AdminGetIdentitiesFilters,
|
||||||
AdminGetUsersFilters,
|
AdminGetUsersFilters,
|
||||||
AdminSlackConfig,
|
AdminSlackConfig,
|
||||||
TGetServerRootKmsEncryptionDetails,
|
TGetServerRootKmsEncryptionDetails,
|
||||||
TServerConfig
|
TServerConfig
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { Identity } from "@app/hooks/api/identities/types.ts";
|
||||||
|
|
||||||
export const adminStandaloneKeys = {
|
export const adminStandaloneKeys = {
|
||||||
getUsers: "get-users"
|
getUsers: "get-users",
|
||||||
|
getIdentities: "get-identities"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const adminQueryKeys = {
|
export const adminQueryKeys = {
|
||||||
serverConfig: () => ["server-config"] as const,
|
serverConfig: () => ["server-config"] as const,
|
||||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||||
|
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||||
|
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] 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 = () => {
|
export const useGetAdminSlackConfig = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: adminQueryKeys.getAdminSlackConfig(),
|
queryKey: adminQueryKeys.getAdminSlackConfig(),
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ export type AdminGetUsersFilters = {
|
|||||||
adminsOnly: boolean;
|
adminsOnly: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminGetIdentitiesFilters = {
|
||||||
|
limit: number;
|
||||||
|
searchTerm: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AdminSlackConfig = {
|
export type AdminSlackConfig = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { EncryptionPanel } from "./components/EncryptionPanel";
|
|||||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||||
import { RateLimitPanel } from "./components/RateLimitPanel";
|
import { RateLimitPanel } from "./components/RateLimitPanel";
|
||||||
import { UserPanel } from "./components/UserPanel";
|
import { UserPanel } from "./components/UserPanel";
|
||||||
|
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel.tsx";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
@@ -42,6 +43,7 @@ enum TabSections {
|
|||||||
RateLimit = "rate-limit",
|
RateLimit = "rate-limit",
|
||||||
Integrations = "integrations",
|
Integrations = "integrations",
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
Identities = "identities",
|
||||||
Kmip = "kmip"
|
Kmip = "kmip"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
|
|||||||
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
||||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||||
<Tab value={TabSections.Users}>Users</Tab>
|
<Tab value={TabSections.Users}>Users</Tab>
|
||||||
|
<Tab value={TabSections.Identities}>Identities</Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Settings}>
|
<TabPanel value={TabSections.Settings}>
|
||||||
@@ -409,6 +412,9 @@ export const OverviewPage = () => {
|
|||||||
<TabPanel value={TabSections.Users}>
|
<TabPanel value={TabSections.Users}>
|
||||||
<UserPanel />
|
<UserPanel />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Identities}>
|
||||||
|
<IdentityPanel />
|
||||||
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={searchIdentityFilter}
|
||||||
|
onChange={(e) => setSearchIdentityFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search identities by name..."
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-1/2">Name</Th>
|
||||||
|
<Th className="w-1/2">Auth Method</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
|
||||||
|
{!isPending &&
|
||||||
|
data?.pages?.map((identities) =>
|
||||||
|
identities.map(({ name, authMethod, id }) => (
|
||||||
|
<Tr key={`identity-${id}`} className="w-full">
|
||||||
|
<Td className="w-1/2">{name}</Td>
|
||||||
|
<Td className="w-1/2">
|
||||||
|
{authMethod || <span className={"italic text-mineshaft-400"}>None</span>}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faServer} />}
|
||||||
|
</TableContainer>
|
||||||
|
{!isEmpty && (
|
||||||
|
<Button
|
||||||
|
className="mt-4 py-3 text-sm"
|
||||||
|
isFullWidth
|
||||||
|
variant="star"
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
>
|
||||||
|
{hasNextPage ? "Load More" : "End of list"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityPanel = () => (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||||
|
</div>
|
||||||
|
<IdentityPanelTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -61,12 +61,12 @@ const UserPanelTable = ({
|
|||||||
const [adminsOnly, setAdminsOnly] = useState(false);
|
const [adminsOnly, setAdminsOnly] = useState(false);
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const userId = user?.id || "";
|
const userId = user?.id || "";
|
||||||
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
|
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
|
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
|
||||||
limit: 20,
|
limit: 20,
|
||||||
searchTerm: debounedSearchTerm,
|
searchTerm: debouncedSearchTerm,
|
||||||
adminsOnly
|
adminsOnly
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user