mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 14:27:59 +00:00
feat: completed api for new search identities
This commit is contained in:
@@ -66,6 +66,17 @@ export const IDENTITIES = {
|
|||||||
},
|
},
|
||||||
LIST: {
|
LIST: {
|
||||||
orgId: "The ID of the organization to list identities."
|
orgId: "The ID of the organization to list identities."
|
||||||
|
},
|
||||||
|
SEARCH: {
|
||||||
|
search: {
|
||||||
|
desc: "The filters to apply to the search.",
|
||||||
|
name: "The name of the identity to filter by.",
|
||||||
|
role: "The organizational role of the identity to filter by."
|
||||||
|
},
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
|
||||||
|
limit: "The number of identities to return.",
|
||||||
|
orderBy: "The column to order identities by.",
|
||||||
|
orderDirection: "The direction to order identities in."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SearchResourceOperators, TSearchResourceOperator } from "./search";
|
||||||
|
|
||||||
|
const buildKnexQuery = (
|
||||||
|
query: Knex.QueryBuilder,
|
||||||
|
// when it's multiple table field means it's field1 or field2
|
||||||
|
fields: string | string[],
|
||||||
|
operator: SearchResourceOperators,
|
||||||
|
value: unknown
|
||||||
|
) => {
|
||||||
|
switch (operator) {
|
||||||
|
case SearchResourceOperators.$eq: {
|
||||||
|
if (typeof value !== "string" && typeof value !== "number")
|
||||||
|
throw new Error("Invalid value type for $eq operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.where(fields, "=", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.where(el, "=", value);
|
||||||
|
}
|
||||||
|
return void qb.orWhere(el, "=", value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchResourceOperators.$neq: {
|
||||||
|
if (typeof value !== "string" && typeof value !== "number")
|
||||||
|
throw new Error("Invalid value type for $neq operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.where(fields, "<>", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.where(el, "<>", value);
|
||||||
|
}
|
||||||
|
return void qb.orWhere(el, "<>", value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case SearchResourceOperators.$in: {
|
||||||
|
if (!Array.isArray(value)) throw new Error("Invalid value type for $in operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.whereIn(fields, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.whereIn(el, value);
|
||||||
|
}
|
||||||
|
return void qb.orWhereIn(el, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case SearchResourceOperators.$contains: {
|
||||||
|
if (typeof value !== "string") throw new Error("Invalid value type for $contains operator");
|
||||||
|
|
||||||
|
if (typeof fields === "string") {
|
||||||
|
return void query.whereILike(fields, `%${value}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return void query.where((qb) => {
|
||||||
|
return fields.forEach((el, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return void qb.whereILike(el, `%${value}%`);
|
||||||
|
}
|
||||||
|
return void qb.orWhereILike(el, `%${value}%`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildKnexFilterForSearchResource = <T extends { [K: string]: TSearchResourceOperator }, K extends keyof T>(
|
||||||
|
rootQuery: Knex.QueryBuilder,
|
||||||
|
searchFilter: T & { $or?: T[] },
|
||||||
|
getAttributeField: (attr: K) => string | string[] | null
|
||||||
|
) => {
|
||||||
|
const { $or: orFilters = [] } = searchFilter;
|
||||||
|
(Object.keys(searchFilter) as K[]).forEach((key) => {
|
||||||
|
// akhilmhdh: yes, we could have split in top. This is done to satisfy ts type error
|
||||||
|
if (key === "$or") return;
|
||||||
|
|
||||||
|
const dbField = getAttributeField(key);
|
||||||
|
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
|
||||||
|
|
||||||
|
const dbValue = searchFilter[key];
|
||||||
|
if (typeof dbValue === "string" || typeof dbValue === "number") {
|
||||||
|
buildKnexQuery(rootQuery, dbField, SearchResourceOperators.$eq, dbValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
|
||||||
|
buildKnexQuery(
|
||||||
|
rootQuery,
|
||||||
|
dbField,
|
||||||
|
el as SearchResourceOperators,
|
||||||
|
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orFilters.length) {
|
||||||
|
void rootQuery.andWhere((andQb) => {
|
||||||
|
return orFilters.forEach((orFilter) => {
|
||||||
|
return void andQb.orWhere((qb) => {
|
||||||
|
(Object.keys(orFilter) as K[]).forEach((key) => {
|
||||||
|
const dbField = getAttributeField(key);
|
||||||
|
if (!dbField) throw new Error(`DB field not found for ${String(key)}`);
|
||||||
|
|
||||||
|
const dbValue = orFilter[key];
|
||||||
|
if (typeof dbValue === "string" || typeof dbValue === "number") {
|
||||||
|
buildKnexQuery(qb, dbField, SearchResourceOperators.$eq, dbValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(dbValue as Record<string, unknown>).forEach((el) => {
|
||||||
|
buildKnexQuery(
|
||||||
|
qb,
|
||||||
|
dbField,
|
||||||
|
el as SearchResourceOperators,
|
||||||
|
(dbValue as Record<SearchResourceOperators, unknown>)[el as SearchResourceOperators]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export enum SearchResourceOperators {
|
||||||
|
$eq = "$eq",
|
||||||
|
$neq = "$neq",
|
||||||
|
$in = "$in",
|
||||||
|
$contains = "$contains"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchResourceOperatorSchema = z.union([
|
||||||
|
z.string(),
|
||||||
|
z.number(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[SearchResourceOperators.$eq]: z.string().optional(),
|
||||||
|
[SearchResourceOperators.$neq]: z.string().optional(),
|
||||||
|
[SearchResourceOperators.$in]: z.string().array().optional(),
|
||||||
|
[SearchResourceOperators.$contains]: z.string().array().optional()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type TSearchResourceOperator = z.infer<typeof SearchResourceOperatorSchema>;
|
||||||
|
|
||||||
|
export type TSearchResource = {
|
||||||
|
[k: string]: z.ZodOptional<
|
||||||
|
z.ZodUnion<
|
||||||
|
[
|
||||||
|
z.ZodEffects<z.ZodString | z.ZodNumber>,
|
||||||
|
z.ZodObject<{
|
||||||
|
[SearchResourceOperators.$eq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
|
||||||
|
[SearchResourceOperators.$neq]?: z.ZodOptional<z.ZodEffects<z.ZodString | z.ZodNumber>>;
|
||||||
|
[SearchResourceOperators.$in]?: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString | z.ZodNumber>>>;
|
||||||
|
[SearchResourceOperators.$contains]?: z.ZodOptional<z.ZodEffects<z.ZodString>>;
|
||||||
|
}>
|
||||||
|
]
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildSearchZodSchema = <T extends TSearchResource>(schema: z.ZodObject<T>) => {
|
||||||
|
return schema.extend({ $or: schema.array().optional() }).optional();
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export enum CharacterType {
|
export enum CharacterType {
|
||||||
Alphabets = "alphabets",
|
Alphabets = "alphabets",
|
||||||
Numbers = "numbers",
|
Numbers = "numbers",
|
||||||
@@ -101,3 +103,10 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
|||||||
return regex.test(input);
|
return regex.test(input);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const zodValidateCharacters = (allowedCharacters: CharacterType[]) => {
|
||||||
|
const validator = characterValidator(allowedCharacters);
|
||||||
|
return (schema: z.ZodString, fieldName: string) => {
|
||||||
|
return schema.refine(validator, { message: `${fieldName} can only contain ${allowedCharacters.join(",")}` });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,15 +3,26 @@ import { z } from "zod";
|
|||||||
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
|
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { IDENTITIES } from "@app/lib/api-docs";
|
import { IDENTITIES } from "@app/lib/api-docs";
|
||||||
|
import { buildSearchZodSchema, SearchResourceOperators } from "@app/lib/search-resource/search";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
|
||||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
|
const searchResourceZodValidate = zodValidateCharacters([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Spaces,
|
||||||
|
CharacterType.Underscore,
|
||||||
|
CharacterType.Hyphen
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -245,7 +256,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/",
|
url: "/",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
@@ -289,6 +300,101 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/search",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Search identities",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
body: z.object({
|
||||||
|
orderBy: z
|
||||||
|
.nativeEnum(OrgIdentityOrderBy)
|
||||||
|
.default(OrgIdentityOrderBy.Name)
|
||||||
|
.describe(IDENTITIES.SEARCH.orderBy)
|
||||||
|
.optional(),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(IDENTITIES.SEARCH.orderDirection)
|
||||||
|
.optional(),
|
||||||
|
limit: z.number().max(100).default(50).describe(IDENTITIES.SEARCH.limit),
|
||||||
|
offset: z.number().default(0).describe(IDENTITIES.SEARCH.offset),
|
||||||
|
search: buildSearchZodSchema(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
name: z
|
||||||
|
.union([
|
||||||
|
searchResourceZodValidate(z.string().max(255), "Name"),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Name $eq"),
|
||||||
|
[SearchResourceOperators.$contains]: searchResourceZodValidate(
|
||||||
|
z.string().max(255),
|
||||||
|
"Name $contains"
|
||||||
|
),
|
||||||
|
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Name $in").array()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
])
|
||||||
|
.describe(IDENTITIES.SEARCH.search.name),
|
||||||
|
role: z
|
||||||
|
.union([
|
||||||
|
searchResourceZodValidate(z.string().max(255), "Role"),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Role $eq"),
|
||||||
|
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Role $in").array()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
])
|
||||||
|
.describe(IDENTITIES.SEARCH.search.name)
|
||||||
|
})
|
||||||
|
.describe(IDENTITIES.SEARCH.search.desc)
|
||||||
|
.partial()
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identities: IdentityOrgMembershipsSchema.extend({
|
||||||
|
customRole: OrgRolesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
permissions: true,
|
||||||
|
description: true
|
||||||
|
}).optional(),
|
||||||
|
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||||
|
authMethods: z.array(z.string())
|
||||||
|
})
|
||||||
|
}).array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
searchFilter: req.body.search,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
limit: req.body.limit,
|
||||||
|
offset: req.body.offset
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identities: identityMemberships, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:identityId/identity-memberships",
|
url: "/:identityId/identity-memberships",
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ import {
|
|||||||
TIdentityUniversalAuths,
|
TIdentityUniversalAuths,
|
||||||
TOrgRoles
|
TOrgRoles
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
import {
|
||||||
|
OrgIdentityOrderBy,
|
||||||
|
TListOrgIdentitiesByOrgIdDTO,
|
||||||
|
TSearchOrgIdentitiesByOrgIdDAL
|
||||||
|
} from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
import { buildAuthMethods } from "./identity-fns";
|
import { buildAuthMethods } from "./identity-fns";
|
||||||
|
|
||||||
@@ -195,7 +200,6 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
"paginatedIdentity.identityId",
|
"paginatedIdentity.identityId",
|
||||||
`${TableName.IdentityJwtAuth}.identityId`
|
`${TableName.IdentityJwtAuth}.identityId`
|
||||||
)
|
)
|
||||||
|
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema("paginatedIdentity"),
|
db.ref("id").withSchema("paginatedIdentity"),
|
||||||
db.ref("role").withSchema("paginatedIdentity"),
|
db.ref("role").withSchema("paginatedIdentity"),
|
||||||
@@ -309,6 +313,214 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchIdentities = async (
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
offset = 0,
|
||||||
|
orderBy = OrgIdentityOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
searchFilter,
|
||||||
|
orgId
|
||||||
|
}: TSearchOrgIdentitiesByOrgIdDAL,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const searchQuery = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
|
||||||
|
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||||
|
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
|
||||||
|
.select(`${TableName.IdentityOrgMembership}.id`)
|
||||||
|
.select<{ id: string; total_count: string }>(
|
||||||
|
db.raw(
|
||||||
|
`count(${TableName.IdentityOrgMembership}."identityId") OVER(PARTITION BY ${TableName.IdentityOrgMembership}."orgId") as total_count`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.as("searchedIdentities");
|
||||||
|
|
||||||
|
if (searchFilter) {
|
||||||
|
buildKnexFilterForSearchResource(searchQuery, searchFilter, (attr) => {
|
||||||
|
switch (attr) {
|
||||||
|
case "role":
|
||||||
|
return [`${TableName.OrgRoles}.slug`, `${TableName.IdentityOrgMembership}.role`];
|
||||||
|
case "name":
|
||||||
|
return `${TableName.Identity}.name`;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Invalid ${String(attr)} provided` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
void searchQuery.offset(offset).limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TSubquery = Awaited<typeof searchQuery>;
|
||||||
|
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
|
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||||
|
.join<TSubquery>(searchQuery, `${TableName.IdentityOrgMembership}.id`, "searchedIdentities.id")
|
||||||
|
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||||
|
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityUniversalAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityUniversalAuth}.identityId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityGcpAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityGcpAuth}.identityId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityAwsAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityAwsAuth}.identityId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityKubernetesAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityOidcAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityOidcAuth}.identityId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityAzureAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityAzureAuth}.identityId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityTokenAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityTokenAuth}.identityId`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.IdentityJwtAuth,
|
||||||
|
`${TableName.IdentityOrgMembership}.identityId`,
|
||||||
|
`${TableName.IdentityJwtAuth}.identityId`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.IdentityOrgMembership),
|
||||||
|
db.ref("total_count").withSchema("searchedIdentities"),
|
||||||
|
db.ref("role").withSchema(TableName.IdentityOrgMembership),
|
||||||
|
db.ref("roleId").withSchema(TableName.IdentityOrgMembership),
|
||||||
|
db.ref("orgId").withSchema(TableName.IdentityOrgMembership),
|
||||||
|
db.ref("createdAt").withSchema(TableName.IdentityOrgMembership),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
|
||||||
|
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
|
|
||||||
|
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||||
|
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||||
|
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||||
|
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||||
|
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||||
|
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||||
|
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||||
|
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
|
||||||
|
)
|
||||||
|
// cr stands for custom role
|
||||||
|
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("name").as("crName").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||||
|
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orderBy === OrgIdentityOrderBy.Name) {
|
||||||
|
void query.orderBy("identityName", orderDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
const formattedDocs = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: ({
|
||||||
|
crId,
|
||||||
|
crDescription,
|
||||||
|
crSlug,
|
||||||
|
crPermission,
|
||||||
|
crName,
|
||||||
|
identityId,
|
||||||
|
identityName,
|
||||||
|
role,
|
||||||
|
roleId,
|
||||||
|
total_count,
|
||||||
|
id,
|
||||||
|
uaId,
|
||||||
|
awsId,
|
||||||
|
gcpId,
|
||||||
|
jwtId,
|
||||||
|
kubernetesId,
|
||||||
|
oidcId,
|
||||||
|
azureId,
|
||||||
|
tokenId,
|
||||||
|
createdAt,
|
||||||
|
updatedAt
|
||||||
|
}) => ({
|
||||||
|
role,
|
||||||
|
roleId,
|
||||||
|
identityId,
|
||||||
|
id,
|
||||||
|
total_count: total_count as string,
|
||||||
|
orgId,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
customRole: roleId
|
||||||
|
? {
|
||||||
|
id: crId,
|
||||||
|
name: crName,
|
||||||
|
slug: crSlug,
|
||||||
|
permissions: crPermission,
|
||||||
|
description: crDescription
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
identity: {
|
||||||
|
id: identityId,
|
||||||
|
name: identityName,
|
||||||
|
authMethods: buildAuthMethods({
|
||||||
|
uaId,
|
||||||
|
awsId,
|
||||||
|
gcpId,
|
||||||
|
kubernetesId,
|
||||||
|
oidcId,
|
||||||
|
azureId,
|
||||||
|
tokenId,
|
||||||
|
jwtId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return { docs: formattedDocs, totalCount: Number(formattedDocs?.[0]?.total_count ?? 0) };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByOrgId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const countAllOrgIdentities = async (
|
const countAllOrgIdentities = async (
|
||||||
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
|
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
@@ -331,5 +543,5 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities };
|
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities, searchIdentities };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
TGetIdentityByIdDTO,
|
TGetIdentityByIdDTO,
|
||||||
TListOrgIdentitiesByOrgIdDTO,
|
TListOrgIdentitiesByOrgIdDTO,
|
||||||
TListProjectIdentitiesByIdentityIdDTO,
|
TListProjectIdentitiesByIdentityIdDTO,
|
||||||
|
TSearchOrgIdentitiesByOrgIdDTO,
|
||||||
TUpdateIdentityDTO
|
TUpdateIdentityDTO
|
||||||
} from "./identity-types";
|
} from "./identity-types";
|
||||||
|
|
||||||
@@ -288,6 +289,33 @@ export const identityServiceFactory = ({
|
|||||||
return { identityMemberships, totalCount };
|
return { identityMemberships, totalCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchOrgIdentities = async ({
|
||||||
|
orgId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
searchFilter = {}
|
||||||
|
}: TSearchOrgIdentitiesByOrgIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const { totalCount, docs } = await identityOrgMembershipDAL.searchIdentities({
|
||||||
|
orgId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
searchFilter
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityMemberships: docs, totalCount };
|
||||||
|
};
|
||||||
|
|
||||||
const listProjectIdentitiesByIdentityId = async ({
|
const listProjectIdentitiesByIdentityId = async ({
|
||||||
identityId,
|
identityId,
|
||||||
actor,
|
actor,
|
||||||
@@ -317,6 +345,7 @@ export const identityServiceFactory = ({
|
|||||||
deleteIdentity,
|
deleteIdentity,
|
||||||
listOrgIdentities,
|
listOrgIdentities,
|
||||||
getIdentityById,
|
getIdentityById,
|
||||||
|
searchOrgIdentities,
|
||||||
listProjectIdentitiesByIdentityId
|
listProjectIdentitiesByIdentityId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IPType } from "@app/lib/ip";
|
import { IPType } from "@app/lib/ip";
|
||||||
|
import { TSearchResourceOperator } from "@app/lib/search-resource/search";
|
||||||
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TCreateIdentityDTO = {
|
export type TCreateIdentityDTO = {
|
||||||
@@ -46,3 +47,17 @@ export enum OrgIdentityOrderBy {
|
|||||||
Name = "name"
|
Name = "name"
|
||||||
// Role = "role"
|
// Role = "role"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TSearchOrgIdentitiesByOrgIdDAL = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: OrgIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
orgId: string;
|
||||||
|
searchFilter?: Partial<{
|
||||||
|
name: Omit<TSearchResourceOperator, "number">;
|
||||||
|
role: Omit<TSearchResourceOperator, "number">;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSearchOrgIdentitiesByOrgIdDTO = TSearchOrgIdentitiesByOrgIdDAL & TOrgPermission;
|
||||||
|
|||||||
Reference in New Issue
Block a user