mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 04:14:38 +00:00
Merge remote-tracking branch 'origin/main' into ENG-3639
This commit is contained in:
@@ -1,11 +1,115 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { TableName, TAppConnections } from "@app/db/schemas";
|
||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||
import { transformUsageToProjects } from "@app/services/app-connection/app-connection-fns";
|
||||
|
||||
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
|
||||
|
||||
type AppConnectionFindFilter = Parameters<typeof buildFindFilter<TAppConnections>>[0];
|
||||
|
||||
export const appConnectionDALFactory = (db: TDbClient) => {
|
||||
const appConnectionOrm = ormify(db, TableName.AppConnection);
|
||||
|
||||
return { ...appConnectionOrm };
|
||||
const findWithProjectDetails = async (filter: AppConnectionFindFilter, tx?: Knex) => {
|
||||
const query = (tx || db.replicaNode())(TableName.AppConnection)
|
||||
.leftJoin(TableName.Project, `${TableName.AppConnection}.projectId`, `${TableName.Project}.id`)
|
||||
.select(selectAllTableCols(TableName.AppConnection))
|
||||
.select(
|
||||
// project
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.AppConnection, filter)));
|
||||
}
|
||||
|
||||
const connections = await query;
|
||||
|
||||
return connections.map(({ projectName, projectSlug, projectType, projectId, ...connection }) => ({
|
||||
...connection,
|
||||
projectId,
|
||||
project: projectId
|
||||
? {
|
||||
name: projectName,
|
||||
type: projectType,
|
||||
slug: projectSlug,
|
||||
id: projectId
|
||||
}
|
||||
: null
|
||||
}));
|
||||
};
|
||||
|
||||
const findAppConnectionUsageById = async (connectionId: string, tx?: Knex) => {
|
||||
const secretSyncs = await (tx || db.replicaNode())(TableName.SecretSync)
|
||||
.where(`${TableName.SecretSync}.connectionId`, connectionId)
|
||||
.join(TableName.Project, `${TableName.SecretSync}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.SecretSync),
|
||||
db.ref("id").withSchema(TableName.SecretSync),
|
||||
db.ref("projectId").withSchema(TableName.SecretSync),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||
);
|
||||
|
||||
const secretRotations = await (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||
.where(`${TableName.SecretRotationV2}.connectionId`, connectionId)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.SecretRotationV2),
|
||||
db.ref("id").withSchema(TableName.SecretRotationV2),
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||
);
|
||||
|
||||
const externalCas = await (tx || db.replicaNode())(TableName.ExternalCertificateAuthority)
|
||||
.where(`${TableName.ExternalCertificateAuthority}.appConnectionId`, connectionId)
|
||||
.orWhere(`${TableName.ExternalCertificateAuthority}.dnsAppConnectionId`, connectionId)
|
||||
.join(
|
||||
TableName.CertificateAuthority,
|
||||
`${TableName.ExternalCertificateAuthority}.caId`,
|
||||
`${TableName.CertificateAuthority}.id`
|
||||
)
|
||||
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.CertificateAuthority),
|
||||
db.ref("id").withSchema(TableName.ExternalCertificateAuthority),
|
||||
db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority),
|
||||
db.ref("dnsAppConnectionId").withSchema(TableName.ExternalCertificateAuthority),
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||
);
|
||||
|
||||
const dataSources = await (tx || db.replicaNode())(TableName.SecretScanningDataSource)
|
||||
.where(`${TableName.SecretScanningDataSource}.connectionId`, connectionId)
|
||||
.join(TableName.Project, `${TableName.SecretScanningDataSource}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.SecretScanningDataSource),
|
||||
db.ref("id").withSchema(TableName.SecretScanningDataSource),
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("slug").as("projectSlug").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||
);
|
||||
|
||||
return transformUsageToProjects({
|
||||
secretSyncs,
|
||||
secretRotations,
|
||||
dataSources,
|
||||
externalCas
|
||||
});
|
||||
};
|
||||
|
||||
return { ...appConnectionOrm, findAppConnectionUsageById, findWithProjectDetails };
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||
import {
|
||||
getOCIConnectionListItem,
|
||||
@@ -8,6 +9,8 @@ import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-maps";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
@@ -16,6 +19,7 @@ import {
|
||||
validateSqlConnectionCredentials
|
||||
} from "@app/services/app-connection/shared/sql";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { SECRET_SYNC_CONNECTION_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
|
||||
import {
|
||||
getOnePassConnectionListItem,
|
||||
@@ -133,7 +137,22 @@ import {
|
||||
} from "./windmill";
|
||||
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries(
|
||||
Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [value, key])
|
||||
);
|
||||
|
||||
const SECRET_ROTATION_APP_CONNECTION_MAP = Object.fromEntries(
|
||||
Object.entries(SECRET_ROTATION_CONNECTION_MAP).map(([key, value]) => [value, key])
|
||||
);
|
||||
|
||||
const SECRET_SCANNING_APP_CONNECTION_MAP = Object.fromEntries(
|
||||
Object.entries(SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP).map(([key, value]) => [value, key])
|
||||
);
|
||||
|
||||
// scott: ideally this would be derived from a utilized map like the above
|
||||
const PKI_APP_CONNECTIONS = [AppConnection.AWS, AppConnection.Cloudflare, AppConnection.AzureADCS];
|
||||
|
||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
return [
|
||||
getAwsConnectionListItem(),
|
||||
getGitHubConnectionListItem(),
|
||||
@@ -173,22 +192,51 @@ export const listAppConnectionOptions = () => {
|
||||
getDigitalOceanConnectionListItem(),
|
||||
getNetlifyConnectionListItem(),
|
||||
getOktaConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
]
|
||||
.filter((option) => {
|
||||
switch (projectType) {
|
||||
case ProjectType.SecretManager:
|
||||
return (
|
||||
Boolean(SECRET_SYNC_APP_CONNECTION_MAP[option.app]) ||
|
||||
Boolean(SECRET_ROTATION_APP_CONNECTION_MAP[option.app])
|
||||
);
|
||||
case ProjectType.SecretScanning:
|
||||
return Boolean(SECRET_SCANNING_APP_CONNECTION_MAP[option.app]);
|
||||
case ProjectType.CertificateManager:
|
||||
return PKI_APP_CONNECTIONS.includes(option.app);
|
||||
case ProjectType.KMS:
|
||||
return false;
|
||||
case ProjectType.SSH:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
export const encryptAppConnectionCredentials = async ({
|
||||
orgId,
|
||||
credentials,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
}: {
|
||||
orgId: string;
|
||||
credentials: TAppConnection["credentials"];
|
||||
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||
projectId: string | null | undefined;
|
||||
}) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey(
|
||||
projectId
|
||||
? {
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
}
|
||||
: {
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
}
|
||||
);
|
||||
|
||||
const { cipherTextBlob: encryptedCredentialsBlob } = encryptor({
|
||||
plainText: Buffer.from(JSON.stringify(credentials))
|
||||
@@ -200,16 +248,22 @@ export const encryptAppConnectionCredentials = async ({
|
||||
export const decryptAppConnectionCredentials = async ({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
}: {
|
||||
orgId: string;
|
||||
encryptedCredentials: Buffer;
|
||||
kmsService: TAppConnectionServiceFactoryDep["kmsService"];
|
||||
projectId: string | null | undefined;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey(
|
||||
projectId
|
||||
? { type: KmsDataKey.SecretManager, projectId }
|
||||
: {
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
}
|
||||
);
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedCredentials
|
||||
@@ -343,6 +397,7 @@ export const decryptAppConnection = async (
|
||||
credentials: await decryptAppConnectionCredentials({
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
projectId: appConnection.projectId,
|
||||
kmsService
|
||||
}),
|
||||
credentialsHash: crypto.nativeCrypto.createHash("sha256").update(appConnection.encryptedCredentials).digest("hex")
|
||||
@@ -413,3 +468,73 @@ export const enterpriseAppCheck = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
type Resource = {
|
||||
name: string;
|
||||
id: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectSlug: string;
|
||||
projectType: string;
|
||||
};
|
||||
|
||||
type UsageData = {
|
||||
secretSyncs: Resource[];
|
||||
secretRotations: Resource[];
|
||||
dataSources: Resource[];
|
||||
externalCas: Resource[];
|
||||
};
|
||||
|
||||
type ResourceSummary = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type ProjectWithResources = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: ProjectType;
|
||||
resources: {
|
||||
secretSyncs: ResourceSummary[];
|
||||
secretRotations: ResourceSummary[];
|
||||
dataSources: ResourceSummary[];
|
||||
externalCas: (ResourceSummary & { appConnectionId?: string; dnsAppConnectionId?: string })[];
|
||||
};
|
||||
};
|
||||
|
||||
export const transformUsageToProjects = (data: UsageData): ProjectWithResources[] => {
|
||||
const projectMap = new Map<string, ProjectWithResources>();
|
||||
|
||||
Object.entries(data).forEach(([resourceType, resources]) => {
|
||||
resources.forEach((resource) => {
|
||||
const { projectId, projectName, projectSlug, projectType, name, id, ...rest } = resource;
|
||||
|
||||
const projectKey = projectId;
|
||||
|
||||
if (!projectMap.has(projectKey)) {
|
||||
projectMap.set(projectKey, {
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
slug: projectSlug,
|
||||
type: projectType as ProjectType,
|
||||
resources: {
|
||||
secretSyncs: [],
|
||||
secretRotations: [],
|
||||
dataSources: [],
|
||||
externalCas: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const project = projectMap.get(projectKey)!;
|
||||
project.resources[resourceType as keyof ProjectWithResources["resources"]].push({
|
||||
name,
|
||||
id,
|
||||
...rest
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(projectMap.values());
|
||||
};
|
||||
|
||||
@@ -13,7 +13,15 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||
app: true,
|
||||
method: true
|
||||
}).extend({
|
||||
credentialsHash: z.string().optional()
|
||||
credentialsHash: z.string().optional(),
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.nullish()
|
||||
});
|
||||
|
||||
export const GenericCreateAppConnectionFieldsSchema = (
|
||||
@@ -28,6 +36,7 @@ export const GenericCreateAppConnectionFieldsSchema = (
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(AppConnections.CREATE(app).description),
|
||||
projectId: z.string().optional().describe(AppConnections.CREATE(app).projectId),
|
||||
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
||||
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
|
||||
: z
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TAppConnections } from "@app/db/schemas";
|
||||
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
|
||||
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
|
||||
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
|
||||
@@ -14,6 +15,10 @@ import {
|
||||
OrgPermissionSubjects
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionAppConnectionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -27,9 +32,8 @@ import {
|
||||
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
|
||||
validateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
|
||||
import { githubRadarConnectionService } from "@app/services/app-connection/github-radar/github-radar-connection-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { ValidateOnePassConnectionCredentialsSchema } from "./1password";
|
||||
import { onePassConnectionService } from "./1password/1password-connection-service";
|
||||
@@ -41,10 +45,13 @@ import {
|
||||
TAppConnectionConfig,
|
||||
TAppConnectionRaw,
|
||||
TCreateAppConnectionDTO,
|
||||
TGetAppConnectionByNameDTO,
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentialsSchema
|
||||
TValidateAppConnectionCredentialsSchema,
|
||||
TValidateAppConnectionUsageByIdDTO
|
||||
} from "./app-connection-types";
|
||||
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
|
||||
import { auth0ConnectionService } from "./auth0/auth0-connection-service";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas";
|
||||
@@ -73,6 +80,7 @@ import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateGitHubRadarConnectionCredentialsSchema } from "./github-radar";
|
||||
import { githubRadarConnectionService } from "./github-radar/github-radar-connection-service";
|
||||
import { ValidateGitLabConnectionCredentialsSchema } from "./gitlab";
|
||||
import { gitlabConnectionService } from "./gitlab/gitlab-connection-service";
|
||||
import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
|
||||
@@ -108,13 +116,14 @@ import { zabbixConnectionService } from "./zabbix/zabbix-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
gatewayDAL: Pick<TGatewayDALFactory, "find">;
|
||||
gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectById">;
|
||||
};
|
||||
|
||||
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||
@@ -168,29 +177,64 @@ export const appConnectionServiceFactory = ({
|
||||
gatewayService,
|
||||
gatewayV2Service,
|
||||
gatewayDAL,
|
||||
gatewayV2DAL
|
||||
gatewayV2DAL,
|
||||
projectDAL
|
||||
}: TAppConnectionServiceFactoryDep) => {
|
||||
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const listAppConnections = async (actor: OrgServiceActor, app?: AppConnection, projectId?: string) => {
|
||||
let appConnections: TAppConnections[];
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
if (projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
const appConnections = await appConnectionDAL.find(
|
||||
app
|
||||
? { orgId: actor.orgId, app }
|
||||
: {
|
||||
orgId: actor.orgId
|
||||
}
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Read,
|
||||
ProjectPermissionSub.AppConnections
|
||||
);
|
||||
|
||||
appConnections = (
|
||||
await appConnectionDAL.findWithProjectDetails({
|
||||
projectId,
|
||||
...(app ? { app } : {})
|
||||
})
|
||||
).filter((appConnection) =>
|
||||
permission.can(
|
||||
ProjectPermissionAppConnectionActions.Read,
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id })
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
appConnections = (
|
||||
await appConnectionDAL.findWithProjectDetails({
|
||||
orgId: actor.orgId,
|
||||
...(app ? { app } : {})
|
||||
})
|
||||
).filter((appConnection) =>
|
||||
permission.can(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
appConnections
|
||||
@@ -204,18 +248,34 @@ export const appConnectionServiceFactory = ({
|
||||
|
||||
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
if (appConnection.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: appConnection.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Read,
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId })
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId })
|
||||
);
|
||||
}
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||
@@ -223,24 +283,49 @@ export const appConnectionServiceFactory = ({
|
||||
return decryptAppConnection(appConnection, kmsService);
|
||||
};
|
||||
|
||||
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await appConnectionDAL.findOne({ name: connectionName, orgId: actor.orgId });
|
||||
const findAppConnectionByName = async (
|
||||
app: AppConnection,
|
||||
{ connectionName, projectId }: TGetAppConnectionByNameDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appConnection = await appConnectionDAL.findOne({
|
||||
name: connectionName,
|
||||
...(projectId ? { projectId } : { orgId: actor.orgId, projectId: null })
|
||||
});
|
||||
|
||||
if (!appConnection)
|
||||
throw new NotFoundError({ message: `Could not find App Connection with name ${connectionName}` });
|
||||
throw new NotFoundError({
|
||||
message: `Could not find App Connection with name ${connectionName} in ${projectId ? "project" : "organization"} scope.`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
if (appConnection.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: appConnection.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Read,
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id })
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id })
|
||||
);
|
||||
}
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
|
||||
@@ -249,10 +334,10 @@ export const appConnectionServiceFactory = ({
|
||||
};
|
||||
|
||||
const createAppConnection = async (
|
||||
{ method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO,
|
||||
{ method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
@@ -260,13 +345,33 @@ export const appConnectionServiceFactory = ({
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Create,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
if (projectId) {
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
|
||||
if (!project) throw new BadRequestError({ message: `Could not find project with ID ${projectId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Create,
|
||||
ProjectPermissionSub.AppConnections
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Create,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
}
|
||||
|
||||
if (gatewayId) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.AttachGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
@@ -304,7 +409,8 @@ export const appConnectionServiceFactory = ({
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: connectionCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
return appConnectionDAL.create({
|
||||
@@ -313,6 +419,7 @@ export const appConnectionServiceFactory = ({
|
||||
method,
|
||||
app,
|
||||
gatewayId,
|
||||
projectId,
|
||||
...params
|
||||
});
|
||||
};
|
||||
@@ -365,7 +472,7 @@ export const appConnectionServiceFactory = ({
|
||||
"Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections."
|
||||
);
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
@@ -373,13 +480,29 @@ export const appConnectionServiceFactory = ({
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Edit,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
if (appConnection.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: appConnection.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
if (gatewayId !== appConnection.gatewayId) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Edit,
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId })
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Edit,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId })
|
||||
);
|
||||
}
|
||||
|
||||
if (gatewayId !== undefined && gatewayId !== appConnection.gatewayId) {
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.AttachGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
@@ -441,7 +564,8 @@ export const appConnectionServiceFactory = ({
|
||||
? await encryptAppConnectionCredentials({
|
||||
credentials: connectionCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId: appConnection.projectId
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -491,18 +615,34 @@ export const appConnectionServiceFactory = ({
|
||||
|
||||
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
if (appConnection.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: appConnection.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Delete,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Delete,
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId })
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Delete,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId })
|
||||
);
|
||||
}
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||
@@ -544,18 +684,34 @@ export const appConnectionServiceFactory = ({
|
||||
"Failed to connect app due to plan restriction. Upgrade plan to access enterprise app connections."
|
||||
);
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
if (appConnection.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: appConnection.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Connect,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id })
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Connect,
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId })
|
||||
);
|
||||
} else {
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Connect,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId })
|
||||
);
|
||||
}
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({
|
||||
@@ -569,7 +725,23 @@ export const appConnectionServiceFactory = ({
|
||||
return connection as T;
|
||||
};
|
||||
|
||||
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor) => {
|
||||
const validateAppConnectionUsageById = async (
|
||||
app: AppConnection,
|
||||
{ connectionId, projectId }: TValidateAppConnectionUsageByIdDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appConnection = await connectAppConnectionById(app, connectionId, actor);
|
||||
|
||||
if (appConnection.projectId && appConnection.projectId !== projectId) {
|
||||
throw new BadRequestError({
|
||||
message: `You cannot connect project App Connection with ID "${appConnection.id}" from project with ID "${appConnection.projectId}" to project with ID "${projectId}"`
|
||||
});
|
||||
}
|
||||
|
||||
return appConnection;
|
||||
};
|
||||
|
||||
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => {
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
@@ -578,28 +750,89 @@ export const appConnectionServiceFactory = ({
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
const appConnections = await appConnectionDAL.find({ app, orgId: actor.orgId });
|
||||
let availableProjectConnections: TAppConnections[] = [];
|
||||
|
||||
const availableConnections = appConnections.filter((connection) =>
|
||||
if (projectId) {
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
|
||||
if (!project) throw new BadRequestError({ message: `Could not find project with ID ${projectId}` });
|
||||
|
||||
const { permission: projectPermission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(projectPermission).throwUnlessCan(
|
||||
ProjectPermissionAppConnectionActions.Connect,
|
||||
ProjectPermissionSub.AppConnections
|
||||
);
|
||||
|
||||
const projectAppConnections = await appConnectionDAL.find({ app, projectId });
|
||||
|
||||
availableProjectConnections = projectAppConnections.filter((connection) =>
|
||||
projectPermission.can(
|
||||
ProjectPermissionAppConnectionActions.Connect,
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId: connection.id })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgAppConnections = await appConnectionDAL.find({ app, orgId: actor.orgId, projectId: null });
|
||||
|
||||
const availableOrgConnections = orgAppConnections.filter((connection) =>
|
||||
orgPermission.can(
|
||||
OrgPermissionAppConnectionActions.Connect,
|
||||
subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id })
|
||||
)
|
||||
);
|
||||
|
||||
return availableConnections as Omit<TAppConnection, "credentials">[];
|
||||
return [...availableOrgConnections, ...availableProjectConnections].sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
) as Omit<TAppConnection, "credentials">[];
|
||||
};
|
||||
|
||||
const findAppConnectionUsageById = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
if (appConnection.app !== app)
|
||||
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||
|
||||
const projectUsage = await appConnectionDAL.findAppConnectionUsageById(connectionId);
|
||||
|
||||
return projectUsage;
|
||||
};
|
||||
|
||||
return {
|
||||
listAppConnectionOptions,
|
||||
listAppConnectionsByOrg,
|
||||
listAppConnections,
|
||||
findAppConnectionById,
|
||||
findAppConnectionByName,
|
||||
createAppConnection,
|
||||
updateAppConnection,
|
||||
deleteAppConnection,
|
||||
connectAppConnectionById,
|
||||
validateAppConnectionUsageById,
|
||||
listAvailableAppConnectionsForUser,
|
||||
findAppConnectionUsageById,
|
||||
github: githubConnectionService(connectAppConnectionById, gatewayService, gatewayV2Service),
|
||||
githubRadar: githubRadarConnectionService(connectAppConnectionById),
|
||||
gcp: gcpConnectionService(connectAppConnectionById),
|
||||
|
||||
@@ -316,13 +316,23 @@ export type TSqlConnectionInput =
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnectionInput,
|
||||
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId"
|
||||
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId" | "projectId"
|
||||
>;
|
||||
|
||||
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app" | "projectId">> & {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TGetAppConnectionByNameDTO = {
|
||||
connectionName: string;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export type TValidateAppConnectionUsageByIdDTO = {
|
||||
connectionId: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionConfig =
|
||||
| TAwsConnectionConfig
|
||||
| TGitHubConnectionConfig
|
||||
|
||||
@@ -51,7 +51,7 @@ const authorizeAuth0Connection = async ({
|
||||
};
|
||||
|
||||
export const getAuth0ConnectionAccessToken = async (
|
||||
{ id, orgId, credentials }: TAuth0Connection,
|
||||
{ id, orgId, credentials, projectId }: TAuth0Connection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
@@ -72,7 +72,8 @@ export const getAuth0ConnectionAccessToken = async (
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(id, { encryptedCredentials });
|
||||
|
||||
@@ -352,7 +352,8 @@ export const getAzureADCSConnectionCredentials = async (
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
projectId: appConnection.projectId
|
||||
})) as {
|
||||
username: string;
|
||||
password: string;
|
||||
|
||||
+4
@@ -57,6 +57,7 @@ export const getAzureConnectionAccessToken = async (
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
projectId: appConnection.projectId,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureClientSecretsConnectionCredentials;
|
||||
|
||||
@@ -93,6 +94,7 @@ export const getAzureConnectionAccessToken = async (
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
projectId: appConnection.projectId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
@@ -102,6 +104,7 @@ export const getAzureConnectionAccessToken = async (
|
||||
case AzureClientSecretsConnectionMethod.ClientSecret:
|
||||
const accessTokenCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
projectId: appConnection.projectId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureClientSecretsConnectionClientSecretCredentials;
|
||||
@@ -129,6 +132,7 @@ export const getAzureConnectionAccessToken = async (
|
||||
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedClientCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
projectId: appConnection.projectId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,8 @@ export const getAzureDevopsConnection = async (
|
||||
const oauthCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
projectId: appConnection.projectId
|
||||
})) as TAzureDevOpsConnectionCredentials;
|
||||
|
||||
if (!("refreshToken" in oauthCredentials)) {
|
||||
@@ -100,7 +101,8 @@ export const getAzureDevopsConnection = async (
|
||||
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedOAuthCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId: appConnection.projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
|
||||
@@ -111,7 +113,8 @@ export const getAzureDevopsConnection = async (
|
||||
const accessTokenCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
projectId: appConnection.projectId
|
||||
})) as { accessToken: string };
|
||||
|
||||
if (!("accessToken" in accessTokenCredentials)) {
|
||||
@@ -124,7 +127,8 @@ export const getAzureDevopsConnection = async (
|
||||
const clientSecretCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
projectId: appConnection.projectId
|
||||
})) as TAzureDevOpsConnectionClientSecretCredentials;
|
||||
|
||||
const { accessToken, expiresAt, clientId, clientSecret, tenantId: clientTenantId } = clientSecretCredentials;
|
||||
@@ -153,7 +157,8 @@ export const getAzureDevopsConnection = async (
|
||||
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedClientCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId: appConnection.projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
|
||||
|
||||
+8
-4
@@ -58,7 +58,8 @@ export const getAzureConnectionAccessToken = async (
|
||||
const oauthCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
projectId: appConnection.projectId
|
||||
})) as TAzureKeyVaultConnectionCredentials;
|
||||
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
@@ -82,7 +83,8 @@ export const getAzureConnectionAccessToken = async (
|
||||
const encryptedOAuthCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedOAuthCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId: appConnection.projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedOAuthCredentials });
|
||||
@@ -95,7 +97,8 @@ export const getAzureConnectionAccessToken = async (
|
||||
const clientSecretCredentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
projectId: appConnection.projectId
|
||||
})) as TAzureKeyVaultConnectionClientSecretCredentials;
|
||||
|
||||
const { accessToken, expiresAt, clientId, clientSecret, tenantId } = clientSecretCredentials;
|
||||
@@ -124,7 +127,8 @@ export const getAzureConnectionAccessToken = async (
|
||||
const encryptedClientCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedClientCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId: appConnection.projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
|
||||
|
||||
@@ -40,7 +40,7 @@ const authorizeCamundaConnection = async ({
|
||||
};
|
||||
|
||||
export const getCamundaConnectionAccessToken = async (
|
||||
{ id, orgId, credentials }: TCamundaConnection,
|
||||
{ id, orgId, credentials, projectId }: TCamundaConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
@@ -61,7 +61,8 @@ export const getCamundaConnectionAccessToken = async (
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(id, { encryptedCredentials });
|
||||
|
||||
@@ -47,7 +47,7 @@ const authorizeDatabricksConnection = async ({
|
||||
};
|
||||
|
||||
export const getDatabricksConnectionAccessToken = async (
|
||||
{ id, orgId, credentials }: TDatabricksConnection,
|
||||
{ id, orgId, credentials, projectId }: TDatabricksConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
@@ -68,7 +68,8 @@ export const getDatabricksConnectionAccessToken = async (
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(id, { encryptedCredentials });
|
||||
|
||||
@@ -64,6 +64,7 @@ export const refreshGitLabToken = async (
|
||||
refreshToken: string,
|
||||
appId: string,
|
||||
orgId: string,
|
||||
projectId: string | undefined | null,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
|
||||
instanceUrl?: string
|
||||
@@ -105,7 +106,8 @@ export const refreshGitLabToken = async (
|
||||
expiresAt
|
||||
},
|
||||
orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appId, { encryptedCredentials });
|
||||
@@ -238,6 +240,7 @@ export const getGitLabConnectionClient = async (
|
||||
appConnection.credentials.refreshToken,
|
||||
appConnection.id,
|
||||
appConnection.orgId,
|
||||
appConnection.projectId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
appConnection.credentials.instanceUrl
|
||||
@@ -273,6 +276,7 @@ export const listGitLabProjects = async ({
|
||||
appConnection.credentials.refreshToken,
|
||||
appConnection.id,
|
||||
appConnection.orgId,
|
||||
appConnection.projectId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
appConnection.credentials.instanceUrl
|
||||
@@ -341,6 +345,7 @@ export const listGitLabGroups = async ({
|
||||
appConnection.credentials.refreshToken,
|
||||
appConnection.id,
|
||||
appConnection.orgId,
|
||||
appConnection.projectId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
appConnection.credentials.instanceUrl
|
||||
|
||||
@@ -36,6 +36,7 @@ export const refreshHerokuToken = async (
|
||||
refreshToken: string,
|
||||
appId: string,
|
||||
orgId: string,
|
||||
projectId: string | null | undefined,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<string> => {
|
||||
@@ -64,7 +65,8 @@ export const refreshHerokuToken = async (
|
||||
expiresAt: new Date(Date.now() + data.expires_in * 1000 - 60000)
|
||||
},
|
||||
orgId,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(appId, { encryptedCredentials });
|
||||
@@ -186,6 +188,7 @@ export const listHerokuApps = async ({
|
||||
appConnection.credentials.refreshToken,
|
||||
appConnection.id,
|
||||
appConnection.orgId,
|
||||
appConnection.projectId,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
|
||||
@@ -14,6 +14,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
|
||||
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||
@@ -47,6 +49,7 @@ type TAuthLoginServiceFactoryDep = {
|
||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
orgMembershipDAL: TOrgMembershipDALFactory;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@@ -57,7 +60,8 @@ export const authLoginServiceFactory = ({
|
||||
orgDAL,
|
||||
orgMembershipDAL,
|
||||
totpService,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
notificationService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
/*
|
||||
* Private
|
||||
@@ -71,6 +75,16 @@ export const authLoginServiceFactory = ({
|
||||
if (!isDeviceSeen) {
|
||||
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
|
||||
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: user.id,
|
||||
type: NotificationType.LOGIN_FROM_NEW_DEVICE,
|
||||
title: "Login From New Device",
|
||||
body: `A new device with IP **${ip}** and User Agent **${userAgent}** has logged into your account.`
|
||||
}
|
||||
]);
|
||||
|
||||
if (user.email) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.NewDeviceJoin,
|
||||
@@ -563,6 +577,18 @@ export const authLoginServiceFactory = ({
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (adminEmails.length > 0) {
|
||||
await notificationService.createUserNotifications(
|
||||
orgAdmins
|
||||
.filter((admin) => admin.user.id !== user.id)
|
||||
.map((admin) => ({
|
||||
userId: admin.user.id,
|
||||
orgId: organizationId,
|
||||
type: NotificationType.ADMIN_SSO_BYPASS,
|
||||
title: "Security Alert: Admin SSO Bypass",
|
||||
body: `The org admin **${user.email}** has bypassed enforced SSO login.`
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: adminEmails,
|
||||
subjectLine: "Security Alert: Admin SSO Bypass",
|
||||
|
||||
@@ -42,10 +42,10 @@ import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/
|
||||
|
||||
type TAcmeCertificateAuthorityFnsDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa"
|
||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
|
||||
>;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||
@@ -152,7 +152,11 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor);
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
appConnection.app as AppConnection,
|
||||
{ connectionId: dnsAppConnectionId, projectId },
|
||||
actor
|
||||
);
|
||||
|
||||
const caEntity = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
try {
|
||||
@@ -242,10 +246,16 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(id);
|
||||
|
||||
if (!ca) {
|
||||
throw new NotFoundError({ message: `Could not find Certificate Authority with ID "${id}"` });
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.connectAppConnectionById(
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
appConnection.app as AppConnection,
|
||||
dnsAppConnectionId,
|
||||
{ connectionId: dnsAppConnectionId, projectId: ca.projectId },
|
||||
actor
|
||||
);
|
||||
|
||||
|
||||
+12
-6
@@ -41,10 +41,10 @@ import {
|
||||
|
||||
type TAzureAdCsCertificateAuthorityFnsDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa"
|
||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
|
||||
>;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||
@@ -621,9 +621,9 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
await appConnectionService.connectAppConnectionById(
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
appConnection.app as AppConnection,
|
||||
azureAdcsConnectionId,
|
||||
{ connectionId: azureAdcsConnectionId, projectId },
|
||||
actor
|
||||
);
|
||||
|
||||
@@ -705,9 +705,15 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
await appConnectionService.connectAppConnectionById(
|
||||
const ca = await certificateAuthorityDAL.findById(id);
|
||||
|
||||
if (!ca) {
|
||||
throw new NotFoundError({ message: `Could not find Certificate Authority with ID "${id}"` });
|
||||
}
|
||||
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
appConnection.app as AppConnection,
|
||||
azureAdcsConnectionId,
|
||||
{ connectionId: azureAdcsConnectionId, projectId: ca.projectId },
|
||||
actor
|
||||
);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
type TCertificateAuthorityQueueFactoryDep = {
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
certificateAuthorityCrlDAL: TCertificateAuthorityCrlDALFactory;
|
||||
|
||||
@@ -43,7 +43,7 @@ import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-cert
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
| "transaction"
|
||||
|
||||
@@ -55,7 +55,7 @@ export const importDataIntoInfisicalFn = async ({
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workspaceName: project.name,
|
||||
projectName: project.name,
|
||||
createDefaultEnvs: false,
|
||||
tx
|
||||
})
|
||||
|
||||
@@ -5,6 +5,8 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectServiceFactory } from "../project/project-service";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
@@ -42,6 +44,7 @@ export type TExternalMigrationQueueFactoryDep = {
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
|
||||
@@ -62,9 +65,12 @@ export const externalMigrationQueueFactory = ({
|
||||
folderDAL,
|
||||
folderCommitService,
|
||||
folderVersionDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
notificationService
|
||||
}: TExternalMigrationQueueFactoryDep) => {
|
||||
const startImport = async (dto: {
|
||||
orgId: string;
|
||||
actorId: string;
|
||||
actorEmail: string;
|
||||
importType: ExternalPlatforms;
|
||||
data: {
|
||||
@@ -87,9 +93,19 @@ export const externalMigrationQueueFactory = ({
|
||||
};
|
||||
|
||||
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
|
||||
const { data, actorEmail, importType } = job.data;
|
||||
const { data, actorEmail, importType, actorId, orgId } = job.data;
|
||||
|
||||
try {
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_STARTED,
|
||||
title: "Import Started",
|
||||
body: `An import from **${importType}** to Infisical has been started.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import started",
|
||||
@@ -137,6 +153,16 @@ export const externalMigrationQueueFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_SUCCESSFUL,
|
||||
title: "Import Successful",
|
||||
body: `An import from **${importType}** to Infisical has successfully completed.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import successful",
|
||||
@@ -146,6 +172,17 @@ export const externalMigrationQueueFactory = ({
|
||||
template: SmtpTemplates.ExternalImportSuccessful
|
||||
});
|
||||
} catch (err) {
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_FAILED,
|
||||
title: "Import Failed",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
body: `An import from **${importType}** to Infisical has failed: ${(err as any)?.message || "Unknown error"}.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [job.data.actorEmail],
|
||||
subjectLine: "Infisical import failed",
|
||||
|
||||
@@ -73,6 +73,8 @@ export const externalMigrationServiceFactory = ({
|
||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorId: user.id,
|
||||
orgId: actorOrgId,
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.EnvKey,
|
||||
data: {
|
||||
@@ -131,6 +133,8 @@ export const externalMigrationServiceFactory = ({
|
||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorId: user.id,
|
||||
orgId: actorOrgId,
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.Vault,
|
||||
data: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template";
|
||||
@@ -15,10 +16,18 @@ import {
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import {
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
PermissionBoundaryError,
|
||||
RateLimitError,
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
@@ -32,6 +41,8 @@ import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal";
|
||||
import {
|
||||
AllowedFieldsSchema,
|
||||
TAttachLdapAuthDTO,
|
||||
TCheckLdapAuthLockoutDTO,
|
||||
TClearLdapAuthLockoutsDTO,
|
||||
TGetLdapAuthDTO,
|
||||
TLoginLdapAuthDTO,
|
||||
TRevokeLdapAuthDTO,
|
||||
@@ -50,10 +61,19 @@ type TIdentityLdapAuthServiceFactoryDep = {
|
||||
kmsService: TKmsServiceFactory;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
|
||||
keyStore: Pick<
|
||||
TKeyStoreFactory,
|
||||
"setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock"
|
||||
>;
|
||||
};
|
||||
|
||||
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
|
||||
|
||||
type LockoutObject = {
|
||||
lockedOut: boolean;
|
||||
failedAttempts: number;
|
||||
};
|
||||
|
||||
export const identityLdapAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
@@ -62,7 +82,8 @@ export const identityLdapAuthServiceFactory = ({
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService,
|
||||
identityAuthTemplateDAL
|
||||
identityAuthTemplateDAL,
|
||||
keyStore
|
||||
}: TIdentityLdapAuthServiceFactoryDep) => {
|
||||
const getLdapConfig = async (identityId: string) => {
|
||||
const identity = await identityDAL.findOne({ id: identityId });
|
||||
@@ -126,13 +147,17 @@ export const identityLdapAuthServiceFactory = ({
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
|
||||
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||
|
||||
if (!identityLdapAuth) {
|
||||
throw new NotFoundError({ message: `Failed to find LDAP auth for identity with ID ${identityId}` });
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
@@ -204,7 +229,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
isActorSuperAdmin,
|
||||
allowedFields
|
||||
allowedFields,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDurationSeconds,
|
||||
lockoutCounterResetSeconds
|
||||
}: TAttachLdapAuthDTO) => {
|
||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||
|
||||
@@ -337,7 +366,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
|
||||
templateId
|
||||
templateId,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDurationSeconds,
|
||||
lockoutCounterResetSeconds
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -363,7 +396,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDurationSeconds,
|
||||
lockoutCounterResetSeconds
|
||||
}: TUpdateLdapAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
@@ -511,7 +548,11 @@ export const identityLdapAuthServiceFactory = ({
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
: undefined,
|
||||
lockoutEnabled,
|
||||
lockoutThreshold,
|
||||
lockoutDurationSeconds,
|
||||
lockoutCounterResetSeconds
|
||||
});
|
||||
|
||||
return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId };
|
||||
@@ -611,12 +652,123 @@ export const identityLdapAuthServiceFactory = ({
|
||||
return revokedIdentityLdapAuth;
|
||||
};
|
||||
|
||||
const withLdapLockout = async <T>(
|
||||
{ identityId, username }: TCheckLdapAuthLockoutDTO,
|
||||
authFn: () => Promise<T>
|
||||
): Promise<T> => {
|
||||
const usernameSlug = slugify(username.trim().toLowerCase());
|
||||
|
||||
const LOCKOUT_KEY = `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:${usernameSlug}`;
|
||||
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
|
||||
try {
|
||||
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 3000, {
|
||||
retryCount: 3,
|
||||
retryDelay: 1500,
|
||||
retryJitter: 100
|
||||
});
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`identity login failed to acquire lock [identityId=${identityId}] [authMethod=${IdentityAuthMethod.LDAP_AUTH}]`
|
||||
);
|
||||
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
|
||||
}
|
||||
|
||||
try {
|
||||
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||
if (lockoutRaw) {
|
||||
const lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||
if (lockout.lockedOut) {
|
||||
throw new UnauthorizedError({
|
||||
message: "This identity auth method is temporarily locked, please try again later"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await authFn();
|
||||
|
||||
await keyStore.deleteItem(LOCKOUT_KEY);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
if ((error as any).status === 401) {
|
||||
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||
if (!identityLdapAuth) {
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
}
|
||||
|
||||
if (identityLdapAuth.lockoutEnabled) {
|
||||
let lockout: LockoutObject = {
|
||||
lockedOut: false,
|
||||
failedAttempts: 0
|
||||
};
|
||||
|
||||
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||
if (lockoutRaw) {
|
||||
lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||
}
|
||||
|
||||
lockout.failedAttempts += 1;
|
||||
if (lockout.failedAttempts >= identityLdapAuth.lockoutThreshold) {
|
||||
lockout.lockedOut = true;
|
||||
}
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
LOCKOUT_KEY,
|
||||
lockout.lockedOut ? identityLdapAuth.lockoutDurationSeconds : identityLdapAuth.lockoutCounterResetSeconds,
|
||||
JSON.stringify(lockout)
|
||||
);
|
||||
}
|
||||
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
};
|
||||
|
||||
const clearLdapAuthLockouts = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TClearLdapAuthLockoutsDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have ldap auth"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const deleted = await keyStore.deleteItems({
|
||||
pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:*`
|
||||
});
|
||||
|
||||
return { deleted, identityId, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
return {
|
||||
attachLdapAuth,
|
||||
getLdapConfig,
|
||||
updateLdapAuth,
|
||||
login,
|
||||
revokeIdentityLdapAuth,
|
||||
getLdapAuth
|
||||
getLdapAuth,
|
||||
withLdapLockout,
|
||||
clearLdapAuthLockouts
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,6 +27,10 @@ export type TAttachLdapAuthDTO = {
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
lockoutEnabled: boolean;
|
||||
lockoutThreshold: number;
|
||||
lockoutDurationSeconds: number;
|
||||
lockoutCounterResetSeconds: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateLdapAuthDTO = {
|
||||
@@ -43,6 +47,10 @@ export type TUpdateLdapAuthDTO = {
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
lockoutEnabled?: boolean;
|
||||
lockoutThreshold?: number;
|
||||
lockoutDurationSeconds?: number;
|
||||
lockoutCounterResetSeconds?: number;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetLdapAuthDTO = {
|
||||
@@ -56,3 +64,12 @@ export type TLoginLdapAuthDTO = {
|
||||
export type TRevokeLdapAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TClearLdapAuthLockoutsDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCheckLdapAuthLockoutDTO = {
|
||||
identityId: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
@@ -84,69 +84,70 @@ export const identityUaServiceFactory = ({
|
||||
|
||||
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
|
||||
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||
if (identityUa.lockoutEnabled) {
|
||||
try {
|
||||
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 500, {
|
||||
retryCount: 3,
|
||||
retryDelay: 300,
|
||||
retryJitter: 100
|
||||
});
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
|
||||
);
|
||||
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
|
||||
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||
|
||||
let lockout: LockoutObject | undefined;
|
||||
if (lockoutRaw) {
|
||||
lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||
}
|
||||
|
||||
if (lockout && lockout.lockedOut) {
|
||||
throw new UnauthorizedError({
|
||||
message: "This identity auth method is temporarily locked, please try again later"
|
||||
});
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
|
||||
const clientSecretPrefix = clientSecret.slice(0, 4);
|
||||
const clientSecretInfo = await identityUaClientSecretDAL.find({
|
||||
identityUAId: identityUa.id,
|
||||
isClientSecretRevoked: false,
|
||||
clientSecretPrefix
|
||||
});
|
||||
|
||||
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
|
||||
for await (const info of clientSecretInfo) {
|
||||
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
|
||||
|
||||
if (isMatch) {
|
||||
validClientSecretInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||
if (!validClientSecretInfo) {
|
||||
if (identityUa.lockoutEnabled) {
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||
try {
|
||||
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, {
|
||||
retryCount: 3,
|
||||
retryDelay: 300,
|
||||
retryJitter: 100
|
||||
});
|
||||
|
||||
let lockout: LockoutObject | undefined;
|
||||
if (lockoutRaw) {
|
||||
lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||
}
|
||||
|
||||
if (lockout && lockout.lockedOut) {
|
||||
throw new UnauthorizedError({
|
||||
message: "This identity auth method is temporarily locked, please try again later"
|
||||
});
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
|
||||
const clientSecretPrefix = clientSecret.slice(0, 4);
|
||||
const clientSecretInfo = await identityUaClientSecretDAL.find({
|
||||
identityUAId: identityUa.id,
|
||||
isClientSecretRevoked: false,
|
||||
clientSecretPrefix
|
||||
});
|
||||
|
||||
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
|
||||
for await (const info of clientSecretInfo) {
|
||||
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
|
||||
|
||||
if (isMatch) {
|
||||
validClientSecretInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validClientSecretInfo) {
|
||||
if (identityUa.lockoutEnabled) {
|
||||
if (!lockout) {
|
||||
// Re-fetch the latest lockout data while holding the lock
|
||||
const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY);
|
||||
if (lockoutRawNew) {
|
||||
lockout = JSON.parse(lockoutRawNew) as LockoutObject;
|
||||
} else {
|
||||
lockout = {
|
||||
lockedOut: false,
|
||||
failedAttempts: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (lockout.lockedOut) {
|
||||
throw new UnauthorizedError({
|
||||
message: "This identity auth method is temporarily locked, please try again later"
|
||||
});
|
||||
}
|
||||
|
||||
lockout.failedAttempts += 1;
|
||||
if (lockout.failedAttempts >= identityUa.lockoutThreshold) {
|
||||
lockout.lockedOut = true;
|
||||
@@ -157,110 +158,121 @@ export const identityUaServiceFactory = ({
|
||||
lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds,
|
||||
JSON.stringify(lockout)
|
||||
);
|
||||
}
|
||||
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
} else if (lockout) {
|
||||
await keyStore.deleteItem(LOCKOUT_KEY);
|
||||
}
|
||||
|
||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||
if (Number(clientSecretTTL) > 0) {
|
||||
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationTime) {
|
||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to expired client secret"
|
||||
});
|
||||
} catch (e) {
|
||||
if (lock === undefined) {
|
||||
logger.info(
|
||||
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
|
||||
);
|
||||
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
if (lock) {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
|
||||
// number of times client secret can be used for
|
||||
// a login operation reached
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
} else if (lockout) {
|
||||
// If credentials are valid, clear any existing lockout record
|
||||
await keyStore.deleteItem(LOCKOUT_KEY);
|
||||
}
|
||||
|
||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||
if (Number(clientSecretTTL) > 0) {
|
||||
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationTime) {
|
||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to client secret usage limit reached"
|
||||
message: "Access denied due to expired client secret"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const accessTokenTTLParams =
|
||||
Number(identityUa.accessTokenPeriod) === 0
|
||||
? {
|
||||
accessTokenTTL: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
|
||||
}
|
||||
: {
|
||||
accessTokenTTL: identityUa.accessTokenPeriod,
|
||||
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
|
||||
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
|
||||
accessTokenMaxTTL: 1000000000
|
||||
};
|
||||
|
||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
|
||||
await identityOrgMembershipDAL.updateById(
|
||||
identityMembershipOrg.id,
|
||||
{
|
||||
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
lastLoginTime: new Date()
|
||||
},
|
||||
tx
|
||||
);
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
identityUAClientSecretId: uaClientSecretDoc.id,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
|
||||
accessTokenPeriod: identityUa.accessTokenPeriod,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
...accessTokenTTLParams
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return newToken;
|
||||
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
|
||||
// number of times client secret can be used for
|
||||
// a login operation reached
|
||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to client secret usage limit reached"
|
||||
});
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = crypto.jwt().sign(
|
||||
const accessTokenTTLParams =
|
||||
Number(identityUa.accessTokenPeriod) === 0
|
||||
? {
|
||||
accessTokenTTL: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
|
||||
}
|
||||
: {
|
||||
accessTokenTTL: identityUa.accessTokenPeriod,
|
||||
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
|
||||
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
|
||||
accessTokenMaxTTL: 1000000000
|
||||
};
|
||||
|
||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
|
||||
await identityOrgMembershipDAL.updateById(
|
||||
identityMembershipOrg.id,
|
||||
{
|
||||
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
lastLoginTime: new Date()
|
||||
},
|
||||
tx
|
||||
);
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
clientSecretId: validClientSecretInfo.id,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
isAccessTokenRevoked: false,
|
||||
identityUAClientSecretId: uaClientSecretDoc.id,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
|
||||
accessTokenPeriod: identityUa.accessTokenPeriod,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
...accessTokenTTLParams
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
identityUa,
|
||||
validClientSecretInfo,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg,
|
||||
...accessTokenTTLParams
|
||||
};
|
||||
} finally {
|
||||
if (lock) await lock.release();
|
||||
}
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = crypto.jwt().sign(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
clientSecretId: validClientSecretInfo.id,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
identityUa,
|
||||
validClientSecretInfo,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg,
|
||||
...accessTokenTTLParams
|
||||
};
|
||||
};
|
||||
|
||||
const attachUniversalAuth = async ({
|
||||
|
||||
@@ -33,7 +33,7 @@ type TIdentityServiceFactoryDep = {
|
||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getKeysByPattern">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getKeysByPattern" | "getItem">;
|
||||
};
|
||||
|
||||
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
||||
@@ -261,12 +261,18 @@ export const identityServiceFactory = ({
|
||||
const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`);
|
||||
|
||||
const activeLockoutAuthMethods = new Set<string>();
|
||||
activeLockouts.forEach((key) => {
|
||||
for await (const key of activeLockouts) {
|
||||
const parts = key.split(":");
|
||||
if (parts.length > 3) {
|
||||
activeLockoutAuthMethods.add(parts[3]);
|
||||
const lockoutRaw = await keyStore.getItem(key);
|
||||
if (lockoutRaw) {
|
||||
const lockout = JSON.parse(lockoutRaw) as { lockedOut: boolean };
|
||||
if (lockout.lockedOut) {
|
||||
activeLockoutAuthMethods.add(parts[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...identity,
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
export enum NotificationType {
|
||||
ACCESS_APPROVAL_REQUEST = "access-approval-request",
|
||||
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated"
|
||||
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated",
|
||||
ACCESS_POLICY_BYPASSED = "access-policy-bypassed",
|
||||
SECRET_CHANGE_REQUEST = "secret-change-request",
|
||||
SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed",
|
||||
SECRET_ROTATION_FAILED = "secret-rotation-failed",
|
||||
SECRET_SCANNING_SECRETS_DETECTED = "secret-scanning-secrets-detected",
|
||||
SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed",
|
||||
LOGIN_FROM_NEW_DEVICE = "login-from-new-device",
|
||||
ADMIN_SSO_BYPASS = "admin-sso-bypass",
|
||||
IMPORT_STARTED = "import-started",
|
||||
IMPORT_SUCCESSFUL = "import-successful",
|
||||
IMPORT_FAILED = "import-failed",
|
||||
DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin",
|
||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||
PROJECT_INVITATION = "project-invitation",
|
||||
SECRET_SYNC_FAILED = "secret-sync-failed"
|
||||
}
|
||||
|
||||
export interface TCreateUserNotificationDTO {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
@@ -20,6 +22,7 @@ type TOrgAdminServiceFactoryDep = {
|
||||
>;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||
@@ -29,7 +32,8 @@ export const orgAdminServiceFactory = ({
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
notificationService
|
||||
}: TOrgAdminServiceFactoryDep) => {
|
||||
const listOrgProjects = async ({
|
||||
actor,
|
||||
@@ -130,23 +134,34 @@ export const orgAdminServiceFactory = ({
|
||||
});
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const filteredProjectMembers = projectMembers
|
||||
.filter(
|
||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||
)
|
||||
.map((el) => el.user.email!)
|
||||
.filter(Boolean);
|
||||
const projectAdmins = projectMembers.filter(
|
||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||
);
|
||||
const mappedProjectAdmins = projectAdmins.map((el) => el.user.email!).filter(Boolean);
|
||||
const actorEmail = projectMembers.find((el) => el.userId === actorId)?.user?.username;
|
||||
|
||||
if (filteredProjectMembers.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||
recipients: filteredProjectMembers,
|
||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
email: projectMembers.find((el) => el.userId === actorId)?.user?.username
|
||||
}
|
||||
});
|
||||
if (actorEmail) {
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN,
|
||||
title: "Direct Project Access Issued",
|
||||
body: `The organization admin **${actorEmail}** has self-issued direct access to the project **${project.name}**.`
|
||||
}))
|
||||
);
|
||||
|
||||
if (mappedProjectAdmins.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||
recipients: mappedProjectAdmins,
|
||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
email: actorEmail
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return { isExistingMember: false, membership: updatedMembership };
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ import { ms } from "@app/lib/ms";
|
||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
@@ -56,6 +58,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
||||
groupProjectDAL: TGroupProjectDALFactory;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
||||
@@ -74,7 +77,8 @@ export const projectMembershipServiceFactory = ({
|
||||
projectDAL,
|
||||
projectKeyDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
notificationService
|
||||
}: TProjectMembershipServiceFactoryDep) => {
|
||||
const getProjectMemberships = async ({
|
||||
actorId,
|
||||
@@ -236,6 +240,16 @@ export const projectMembershipServiceFactory = ({
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
await notificationService.createUserNotifications(
|
||||
orgMembers.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.PROJECT_INVITATION,
|
||||
title: "Project Invitation",
|
||||
body: `You've been invited to join the project **${project.name}**.`
|
||||
}))
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
|
||||
@@ -399,6 +399,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
name?: string;
|
||||
sortBy?: SearchProjectSortBy;
|
||||
sortDir?: SortDirection;
|
||||
projectIds?: string[];
|
||||
}) => {
|
||||
const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto;
|
||||
|
||||
@@ -454,6 +455,11 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
if (dto.name) {
|
||||
void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`);
|
||||
}
|
||||
|
||||
if (dto.projectIds?.length) {
|
||||
void query.whereIn(`${TableName.Project}.id`, dto.projectIds);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
||||
|
||||
@@ -57,6 +57,8 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||
@@ -183,6 +185,7 @@ type TProjectServiceFactoryDep = {
|
||||
>;
|
||||
projectTemplateService: TProjectTemplateServiceFactory;
|
||||
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||
@@ -227,7 +230,8 @@ export const projectServiceFactory = ({
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
reminderService
|
||||
reminderService,
|
||||
notificationService
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -237,8 +241,8 @@ export const projectServiceFactory = ({
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workspaceName,
|
||||
workspaceDescription,
|
||||
projectName: workspaceName,
|
||||
projectDescription: workspaceDescription,
|
||||
slug: projectSlug,
|
||||
kmsKeyId,
|
||||
tx: trx,
|
||||
@@ -254,7 +258,13 @@ export const projectServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
|
||||
if (
|
||||
permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace) &&
|
||||
permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Project)
|
||||
) {
|
||||
throw new ForbiddenRequestError({ message: "You don't have permission to create a project" });
|
||||
}
|
||||
|
||||
const results = await (trx || projectDAL).transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateProject(organization.id)]);
|
||||
@@ -591,7 +601,8 @@ export const projectServiceFactory = ({
|
||||
secretSharing: update.secretSharing,
|
||||
defaultProduct: update.defaultProduct,
|
||||
showSnapshotsLegacy: update.showSnapshotsLegacy,
|
||||
secretDetectionIgnoreValues: update.secretDetectionIgnoreValues
|
||||
secretDetectionIgnoreValues: update.secretDetectionIgnoreValues,
|
||||
pitVersionLimit: update.pitVersionLimit
|
||||
});
|
||||
|
||||
return updatedProject;
|
||||
@@ -684,19 +695,21 @@ export const projectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
auditLogsRetentionDays,
|
||||
workspaceSlug
|
||||
filter
|
||||
}: TUpdateAuditLogsRetentionDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(workspaceSlug, actorOrgId);
|
||||
const project = await projectDAL.findProjectByFilter(filter);
|
||||
const projectId = project.id;
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `Project with slug '${workspaceSlug}' not found`
|
||||
message: `Project not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { hasRole } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
@@ -1806,7 +1819,8 @@ export const projectServiceFactory = ({
|
||||
limit,
|
||||
type,
|
||||
orderBy,
|
||||
orderDirection
|
||||
orderDirection,
|
||||
projectIds
|
||||
}: TSearchProjectsDTO) => {
|
||||
// check user belong to org
|
||||
await permissionService.getOrgPermission(
|
||||
@@ -1822,6 +1836,7 @@ export const projectServiceFactory = ({
|
||||
offset,
|
||||
name,
|
||||
type,
|
||||
projectIds,
|
||||
orgId: permission.orgId,
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
@@ -1913,6 +1928,21 @@ export const projectServiceFactory = ({
|
||||
projectTypeUrl = "cert-management";
|
||||
}
|
||||
|
||||
const callbackPath = `/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectMembers
|
||||
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||
.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.PROJECT_ACCESS_REQUEST,
|
||||
title: "Project Access Request",
|
||||
body: `**${userDetails.firstName} ${userDetails.lastName}** (${userDetails.email}) has requested access to the project **${project.name}**.`,
|
||||
link: callbackPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.ProjectAccessRequest,
|
||||
recipients: filteredProjectMembers,
|
||||
@@ -1923,7 +1953,7 @@ export const projectServiceFactory = ({
|
||||
projectName: project?.name,
|
||||
orgName: org?.name,
|
||||
note: comment,
|
||||
callback_url: `${appCfg.SITE_URL}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`
|
||||
callback_url: `${appCfg.SITE_URL}${callbackPath}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -42,12 +42,13 @@ export type TCreateProjectDTO = {
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorId: string;
|
||||
actorOrgId?: string;
|
||||
workspaceName: string;
|
||||
workspaceDescription?: string;
|
||||
projectName: string;
|
||||
projectDescription?: string;
|
||||
slug?: string;
|
||||
kmsKeyId?: string;
|
||||
createDefaultEnvs?: boolean;
|
||||
template?: string;
|
||||
pitVersionLimit?: number;
|
||||
tx?: Knex;
|
||||
type?: ProjectType;
|
||||
};
|
||||
@@ -78,7 +79,7 @@ export type TUpdateProjectVersionLimitDTO = {
|
||||
|
||||
export type TUpdateAuditLogsRetentionDTO = {
|
||||
auditLogsRetentionDays: number;
|
||||
workspaceSlug: string;
|
||||
filter: Filter;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateProjectNameDTO = {
|
||||
@@ -90,6 +91,7 @@ export type TUpdateProjectDTO = {
|
||||
update: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
pitVersionLimit?: number;
|
||||
autoCapitalization?: boolean;
|
||||
hasDeleteProtection?: boolean;
|
||||
defaultProduct?: ProjectType;
|
||||
@@ -221,6 +223,7 @@ export type TSearchProjectsDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SearchProjectSortBy;
|
||||
projectIds?: string[];
|
||||
orderDirection?: SortDirection;
|
||||
};
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ const getValidAccessToken = async (
|
||||
connection.credentials.refreshToken,
|
||||
connection.id,
|
||||
connection.orgId,
|
||||
connection.projectId,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
connection.credentials.instanceUrl
|
||||
|
||||
@@ -32,6 +32,7 @@ const getValidAuthToken = async (
|
||||
connection.credentials.refreshToken,
|
||||
connection.id,
|
||||
connection.orgId,
|
||||
connection.projectId,
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
|
||||
db.ref("projectId").withSchema(TableName.AppConnection).as("connectionProjectId"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db
|
||||
@@ -67,6 +68,7 @@ const expandSecretSync = (
|
||||
connectionVersion,
|
||||
connectionIsPlatformManagedCredentials,
|
||||
connectionGatewayId,
|
||||
connectionProjectId,
|
||||
...el
|
||||
} = secretSync;
|
||||
|
||||
@@ -86,7 +88,8 @@ const expandSecretSync = (
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
|
||||
gatewayId: connectionGatewayId
|
||||
gatewayId: connectionGatewayId,
|
||||
projectId: connectionProjectId
|
||||
},
|
||||
folder: folder
|
||||
? {
|
||||
|
||||
@@ -61,6 +61,8 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
|
||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
|
||||
@@ -100,6 +102,7 @@ type TSecretSyncQueueFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
type SecretSyncActionJob = Job<
|
||||
@@ -142,7 +145,8 @@ export const secretSyncQueueFactory = ({
|
||||
folderCommitService,
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
notificationService
|
||||
}: TSecretSyncQueueFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -484,13 +488,14 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
try {
|
||||
const {
|
||||
connection: { orgId, encryptedCredentials }
|
||||
connection: { orgId, encryptedCredentials, projectId }
|
||||
} = secretSync;
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
const secretSyncWithCredentials = {
|
||||
@@ -624,13 +629,14 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
try {
|
||||
const {
|
||||
connection: { orgId, encryptedCredentials }
|
||||
connection: { orgId, encryptedCredentials, projectId }
|
||||
} = secretSync;
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
await $importSecrets(
|
||||
@@ -744,13 +750,14 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
try {
|
||||
const {
|
||||
connection: { orgId, encryptedCredentials }
|
||||
connection: { orgId, encryptedCredentials, projectId }
|
||||
} = secretSync;
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectId
|
||||
});
|
||||
|
||||
const secretMap = await $getInfisicalSecrets(secretSync);
|
||||
@@ -895,6 +902,19 @@ export const secretSyncQueueFactory = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((admin) => ({
|
||||
userId: admin.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_SYNC_FAILED,
|
||||
title: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
|
||||
link: syncPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretSyncFailed,
|
||||
@@ -907,7 +927,7 @@ export const secretSyncQueueFactory = ({
|
||||
secretPath: folder?.path,
|
||||
environment: environment?.name,
|
||||
projectName: project.name,
|
||||
syncUrl: `${appCfg.SITE_URL}/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ import { TSecretSyncQueueFactory } from "./secret-sync-queue";
|
||||
type TSecretSyncServiceFactoryDep = {
|
||||
secretSyncDAL: TSecretSyncDALFactory;
|
||||
secretImportDAL: TSecretImportDALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
||||
@@ -267,7 +267,11 @@ export const secretSyncServiceFactory = ({
|
||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
|
||||
|
||||
// validates permission to connect and app is valid for sync destination
|
||||
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
destinationApp,
|
||||
{ connectionId: params.connectionId, projectId },
|
||||
actor
|
||||
);
|
||||
|
||||
try {
|
||||
const secretSync = await secretSyncDAL.create({
|
||||
@@ -362,7 +366,11 @@ export const secretSyncServiceFactory = ({
|
||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
|
||||
|
||||
// validates permission to connect and app is valid for sync destination
|
||||
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
destinationApp,
|
||||
{ connectionId: params.connectionId, projectId: secretSync.projectId },
|
||||
actor
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -446,9 +446,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
}
|
||||
})
|
||||
.where((bd) => {
|
||||
void bd
|
||||
.whereNull(`${TableName.SecretV2}.userId`)
|
||||
.orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
|
||||
void bd.whereNull(`${TableName.SecretV2}.userId`);
|
||||
// scott: removing this as we don't need to count overrides
|
||||
// and there is currently a bug when you move secrets that doesn't move the override so this can skew count
|
||||
// .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
|
||||
})
|
||||
.countDistinct(`${TableName.SecretV2}.key`);
|
||||
|
||||
|
||||
@@ -491,15 +491,16 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secret = sharedSecretToModify;
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
secretTags: secret.tags.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
if (secret.type !== SecretType.Personal)
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
secretTags: secret.tags.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
|
||||
// validate tags
|
||||
// fetch all tags and if not same count throw error meaning one was invalid tags
|
||||
@@ -510,17 +511,18 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags;
|
||||
|
||||
// now check with new ids
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
...(tagsToCheck.length && {
|
||||
secretTags: tagsToCheck.map((el) => el.slug)
|
||||
if (secret.type !== SecretType.Personal)
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
...(tagsToCheck.length && {
|
||||
secretTags: tagsToCheck.map((el) => el.slug)
|
||||
})
|
||||
})
|
||||
})
|
||||
);
|
||||
);
|
||||
|
||||
if (inputSecret.newSecretName) {
|
||||
const doesNewNameSecretExist = await secretDAL.findOne({
|
||||
@@ -727,15 +729,17 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
});
|
||||
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretToDelete.key,
|
||||
secretTags: secretToDelete.tags?.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
|
||||
if (secretToDelete.type !== SecretType.Personal)
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretToDelete.key,
|
||||
secretTags: secretToDelete.tags?.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const deletedSecret = await secretDAL.transaction(async (tx) => {
|
||||
@@ -1679,7 +1683,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
|
||||
|
||||
// get all tags
|
||||
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
|
||||
const sanitizedTagIds = [...new Set(inputSecrets.flatMap(({ tagIds = [] }) => tagIds))];
|
||||
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
|
||||
if (tags.length !== sanitizedTagIds.length)
|
||||
throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` });
|
||||
@@ -1927,7 +1931,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
|
||||
// get all tags
|
||||
const sanitizedTagIds = secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds);
|
||||
const sanitizedTagIds = [...new Set(secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds))];
|
||||
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds, tx) : [];
|
||||
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" });
|
||||
const tagsGroupByID = groupBy(tags, (i) => i.id);
|
||||
@@ -2354,7 +2358,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
secretId
|
||||
secretId,
|
||||
secretVersions: secretVersionsFilter
|
||||
}: TGetSecretVersionsDTO) => {
|
||||
const secret = await secretDAL.findById(secretId);
|
||||
|
||||
@@ -2391,6 +2396,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||
secretId,
|
||||
projectId: folder.projectId,
|
||||
secretVersions: secretVersionsFilter,
|
||||
findOpt: {
|
||||
offset,
|
||||
limit,
|
||||
@@ -2960,7 +2966,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretKey: secretName
|
||||
});
|
||||
|
||||
return { tree: stackTrace, value: expandedValue };
|
||||
return { tree: stackTrace, value: expandedValue, secret };
|
||||
};
|
||||
|
||||
const getAccessibleSecrets = async ({
|
||||
|
||||
@@ -159,6 +159,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
secretId: string;
|
||||
secretVersions?: string[];
|
||||
};
|
||||
|
||||
export type TSecretReference = { environment: string; secretPath: string; secretKey: string };
|
||||
|
||||
@@ -2568,7 +2568,8 @@ export const secretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
secretId
|
||||
secretId,
|
||||
secretVersions: filterSecretVersions
|
||||
}: TGetSecretVersionsDTO) => {
|
||||
const secretVersionV2 = await secretV2BridgeService
|
||||
.getSecretVersions({
|
||||
@@ -2578,7 +2579,8 @@ export const secretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
limit,
|
||||
offset,
|
||||
secretId
|
||||
secretId,
|
||||
secretVersions: filterSecretVersions
|
||||
})
|
||||
.catch((err) => {
|
||||
if ((err as Error).message === "BadRequest: Failed to find secret") {
|
||||
@@ -2970,14 +2972,23 @@ export const secretServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
projectId: inputProjectId
|
||||
}: TMoveSecretsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
let project;
|
||||
if (projectSlug) {
|
||||
project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
} else if (inputProjectId) {
|
||||
project = await projectDAL.findById(inputProjectId);
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `Project with slug '${projectSlug}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const projectId = project.id;
|
||||
if (project.version === ProjectVersion.V3) {
|
||||
return secretV2BridgeService.moveSecrets({
|
||||
sourceEnvironment,
|
||||
@@ -3170,7 +3181,7 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
}
|
||||
const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
|
||||
project.id,
|
||||
projectId,
|
||||
destinationFolder.environment.slug,
|
||||
destinationFolder.path
|
||||
);
|
||||
@@ -3257,7 +3268,7 @@ export const secretServiceFactory = ({
|
||||
}
|
||||
if (locallyUpdatedSecrets.length) {
|
||||
await fnSecretBulkUpdate({
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
folderId: destinationFolder.id,
|
||||
secretVersionDAL,
|
||||
secretDAL,
|
||||
@@ -3300,7 +3311,7 @@ export const secretServiceFactory = ({
|
||||
const locallyDeletedSecrets = decryptedSourceSecrets.map((el) => ({ ...el, operation: SecretOperations.Delete }));
|
||||
|
||||
const sourceFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
|
||||
project.id,
|
||||
projectId,
|
||||
sourceFolder.environment.slug,
|
||||
sourceFolder.path
|
||||
);
|
||||
|
||||
@@ -331,6 +331,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
secretId: string;
|
||||
secretVersions?: string[];
|
||||
};
|
||||
|
||||
export type TSecretReference = { environment: string; secretPath: string };
|
||||
@@ -534,7 +535,8 @@ export type TSyncSecretsDTO<T extends boolean = false> = {
|
||||
});
|
||||
|
||||
export type TMoveSecretsDTO = {
|
||||
projectSlug: string;
|
||||
projectId?: string;
|
||||
projectSlug?: string;
|
||||
sourceEnvironment: string;
|
||||
sourceSecretPath: string;
|
||||
destinationEnvironment: string;
|
||||
|
||||
@@ -32,7 +32,8 @@ export enum PostHogEventTypes {
|
||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||
SignCert = "Sign PKI Certificate",
|
||||
IssueCert = "Issue PKI Certificate",
|
||||
InvalidateCache = "Invalidate Cache"
|
||||
InvalidateCache = "Invalidate Cache",
|
||||
NotificationUpdated = "Notification Updated"
|
||||
}
|
||||
|
||||
export type TSecretModifiedEvent = {
|
||||
@@ -46,7 +47,7 @@ export type TSecretModifiedEvent = {
|
||||
properties: {
|
||||
numberOfSecrets: number;
|
||||
environment: string;
|
||||
workspaceId: string;
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
channel?: string;
|
||||
userAgent?: string;
|
||||
@@ -232,6 +233,14 @@ export type TInvalidateCacheEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TNotificationUpdatedEvent = {
|
||||
event: PostHogEventTypes.NotificationUpdated;
|
||||
properties: {
|
||||
notificationId: string;
|
||||
isRead?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
@@ -251,4 +260,5 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TSignCertificateEvent
|
||||
| TIssueCertificateEvent
|
||||
| TInvalidateCacheEvent
|
||||
| TNotificationUpdatedEvent
|
||||
);
|
||||
|
||||
@@ -294,11 +294,18 @@ export const userServiceFactory = ({
|
||||
// Delete all user aliases since the email is changing
|
||||
await userAliasDAL.delete({ userId }, tx);
|
||||
|
||||
// Ensure EMAIL auth method is included if not already present
|
||||
const currentAuthMethods = user.authMethods || [];
|
||||
const updatedAuthMethods = currentAuthMethods.includes(AuthMethod.EMAIL)
|
||||
? currentAuthMethods
|
||||
: [...currentAuthMethods, AuthMethod.EMAIL];
|
||||
|
||||
const updatedUser = await userDAL.updateById(
|
||||
userId,
|
||||
{
|
||||
email: newEmail.toLowerCase(),
|
||||
username: newEmail.toLowerCase()
|
||||
username: newEmail.toLowerCase(),
|
||||
authMethods: updatedAuthMethods
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user