mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 23:52:09 +00:00
deconflict merge
This commit is contained in:
@@ -11,7 +11,8 @@ export enum AppConnection {
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill"
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TAppConnectionCredentialsValidator,
|
||||
TAppConnectionTransitionCredentialsToPlatform
|
||||
} from "./app-connection-types";
|
||||
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
|
||||
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
@@ -69,7 +70,8 @@ export const listAppConnectionOptions = () => {
|
||||
getPostgresConnectionListItem(),
|
||||
getMsSqlConnectionListItem(),
|
||||
getCamundaConnectionListItem(),
|
||||
getWindmillConnectionListItem()
|
||||
getWindmillConnectionListItem(),
|
||||
getAuth0ConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -115,26 +117,29 @@ export const decryptAppConnectionCredentials = async ({
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||
};
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureAppConfiguration]:
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
export const validateAppConnectionCredentials = async (
|
||||
appConnection: TAppConnectionConfig
|
||||
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
): Promise<TAppConnection["credentials"]> => {
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureAppConfiguration]:
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||
switch (method) {
|
||||
@@ -163,6 +168,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@@ -206,5 +213,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -13,5 +13,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Postgres]: "PostgreSQL",
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Windmill]: "Windmill"
|
||||
[AppConnection.Windmill]: "Windmill",
|
||||
[AppConnection.Auth0]: "Auth0"
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentialsSchema
|
||||
} from "./app-connection-types";
|
||||
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
@@ -71,7 +73,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema
|
||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -446,6 +449,7 @@ export const appConnectionServiceFactory = ({
|
||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById)
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,12 @@ import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sq
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { AWSRegion } from "./app-connection-enums";
|
||||
import {
|
||||
TAuth0Connection,
|
||||
TAuth0ConnectionConfig,
|
||||
TAuth0ConnectionInput,
|
||||
TValidateAuth0ConnectionCredentialsSchema
|
||||
} from "./auth0";
|
||||
import {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
@@ -90,6 +96,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TMsSqlConnection
|
||||
| TCamundaConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -110,6 +117,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TMsSqlConnectionInput
|
||||
| TCamundaConnectionInput
|
||||
| TWindmillConnectionInput
|
||||
| TAuth0ConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
@@ -135,7 +143,8 @@ export type TAppConnectionConfig =
|
||||
| TVercelConnectionConfig
|
||||
| TSqlConnectionConfig
|
||||
| TCamundaConnectionConfig
|
||||
| TWindmillConnectionConfig;
|
||||
| TWindmillConnectionConfig
|
||||
| TAuth0ConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -150,7 +159,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateCamundaConnectionCredentialsSchema
|
||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema
|
||||
| TValidateWindmillConnectionCredentialsSchema;
|
||||
| TValidateWindmillConnectionCredentialsSchema
|
||||
| TValidateAuth0ConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum Auth0ConnectionMethod {
|
||||
ClientCredentials = "client-credentials"
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
|
||||
import { TAuth0AccessTokenResponse, TAuth0Connection, TAuth0ConnectionConfig } from "./auth0-connection-types";
|
||||
|
||||
export const getAuth0ConnectionListItem = () => {
|
||||
return {
|
||||
name: "Auth0" as const,
|
||||
app: AppConnection.Auth0 as const,
|
||||
methods: Object.values(Auth0ConnectionMethod) as [Auth0ConnectionMethod.ClientCredentials]
|
||||
};
|
||||
};
|
||||
|
||||
const authorizeAuth0Connection = async ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
domain,
|
||||
audience
|
||||
}: TAuth0ConnectionConfig["credentials"]) => {
|
||||
const instanceUrl = domain.startsWith("http") ? domain : `https://${domain}`;
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
const { data } = await request.request<TAuth0AccessTokenResponse>({
|
||||
method: "POST",
|
||||
url: `${removeTrailingSlash(instanceUrl)}/oauth/token`,
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
data: new URLSearchParams({
|
||||
grant_type: "client_credentials", // this will need to be resolved if we support methods other than client credentials
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
audience
|
||||
})
|
||||
});
|
||||
|
||||
if (data.token_type !== "Bearer") {
|
||||
throw new Error(`Unhandled token type: ${data.token_type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
// cap token lifespan to 10 minutes
|
||||
expiresAt: Math.min(data.expires_in * 1000, 600000) + Date.now()
|
||||
};
|
||||
};
|
||||
|
||||
export const getAuth0ConnectionAccessToken = async (
|
||||
{ id, orgId, credentials }: TAuth0Connection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const { expiresAt, accessToken } = credentials;
|
||||
|
||||
// get new token if expired or less than 5 minutes until expiry
|
||||
if (Date.now() < expiresAt - 300000) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
const authData = await authorizeAuth0Connection(credentials);
|
||||
|
||||
const updatedCredentials: TAuth0Connection["credentials"] = {
|
||||
...credentials,
|
||||
...authData
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(id, { encryptedCredentials });
|
||||
|
||||
return authData.accessToken;
|
||||
};
|
||||
|
||||
export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0ConnectionConfig) => {
|
||||
try {
|
||||
const { accessToken, expiresAt } = await authorizeAuth0Connection(credentials);
|
||||
|
||||
return {
|
||||
...credentials,
|
||||
accessToken,
|
||||
expiresAt
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: (e as Error).message ?? `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
|
||||
|
||||
export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({
|
||||
domain: z.string().trim().min(1, "Domain required").describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.domain),
|
||||
clientId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client ID required")
|
||||
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientId),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Secret required")
|
||||
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientSecret),
|
||||
audience: z
|
||||
.string()
|
||||
.trim()
|
||||
.url()
|
||||
.min(1, "Audience required")
|
||||
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.audience)
|
||||
});
|
||||
|
||||
const Auth0ConnectionClientCredentialsOutputCredentialsSchema = z
|
||||
.object({
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
})
|
||||
.merge(Auth0ConnectionClientCredentialsInputCredentialsSchema);
|
||||
|
||||
const BaseAuth0ConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.Auth0)
|
||||
});
|
||||
|
||||
export const Auth0ConnectionSchema = z.intersection(
|
||||
BaseAuth0ConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
|
||||
credentials: Auth0ConnectionClientCredentialsOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAuth0ConnectionSchema.extend({
|
||||
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
|
||||
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.pick({
|
||||
domain: true,
|
||||
clientId: true,
|
||||
audience: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(Auth0ConnectionMethod.ClientCredentials)
|
||||
.describe(AppConnections.CREATE(AppConnection.Auth0).method),
|
||||
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Auth0).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAuth0ConnectionSchema = ValidateAuth0ConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Auth0)
|
||||
);
|
||||
|
||||
export const UpdateAuth0ConnectionSchema = z
|
||||
.object({
|
||||
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Auth0).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0));
|
||||
|
||||
export const Auth0ConnectionListItemSchema = z.object({
|
||||
name: z.literal("Auth0"),
|
||||
app: z.literal(AppConnection.Auth0),
|
||||
// the below is preferable but currently breaks with our zod to json schema parser
|
||||
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||
methods: z.nativeEnum(Auth0ConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0/auth0-connection-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAuth0Connection, TAuth0ListClient, TAuth0ListClientsResponse } from "./auth0-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TAuth0Connection>;
|
||||
|
||||
const listAuth0Clients = async (
|
||||
appConnection: TAuth0Connection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const accessToken = await getAuth0ConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
|
||||
|
||||
const { audience, clientId: connectionClientId } = appConnection.credentials;
|
||||
await blockLocalAndPrivateIpAddresses(audience);
|
||||
|
||||
const clients: TAuth0ListClient[] = [];
|
||||
let hasMore = true;
|
||||
let page = 0;
|
||||
|
||||
while (hasMore) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: clientsPage } = await request.get<TAuth0ListClientsResponse>(`${audience}clients`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
},
|
||||
params: {
|
||||
include_totals: true,
|
||||
per_page: 100,
|
||||
page
|
||||
}
|
||||
});
|
||||
|
||||
clients.push(...clientsPage.clients);
|
||||
page += 1;
|
||||
hasMore = clientsPage.total > clients.length;
|
||||
}
|
||||
|
||||
return (
|
||||
clients.filter((client) => client.client_id !== connectionClientId && client.name !== "All Applications") ?? []
|
||||
);
|
||||
};
|
||||
|
||||
export const auth0ConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const listClients = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Auth0, connectionId, actor);
|
||||
|
||||
const clients = await listAuth0Clients(appConnection, appConnectionDAL, kmsService);
|
||||
|
||||
return clients.map((client) => ({ id: client.client_id, name: client.name }));
|
||||
};
|
||||
|
||||
return {
|
||||
listClients
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import {
|
||||
Auth0ConnectionSchema,
|
||||
CreateAuth0ConnectionSchema,
|
||||
ValidateAuth0ConnectionCredentialsSchema
|
||||
} from "./auth0-connection-schemas";
|
||||
|
||||
export type TAuth0Connection = z.infer<typeof Auth0ConnectionSchema>;
|
||||
|
||||
export type TAuth0ConnectionInput = z.infer<typeof CreateAuth0ConnectionSchema> & {
|
||||
app: AppConnection.Auth0;
|
||||
};
|
||||
|
||||
export type TValidateAuth0ConnectionCredentialsSchema = typeof ValidateAuth0ConnectionCredentialsSchema;
|
||||
|
||||
export type TAuth0ConnectionConfig = DiscriminativePick<TAuth0Connection, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TAuth0AccessTokenResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
export type TAuth0ListClient = {
|
||||
name: string;
|
||||
client_id: string;
|
||||
};
|
||||
|
||||
export type TAuth0ListClientsResponse = {
|
||||
total: number;
|
||||
clients: TAuth0ListClient[];
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./auth0-connection-enums";
|
||||
export * from "./auth0-connection-fns";
|
||||
export * from "./auth0-connection-schemas";
|
||||
export * from "./auth0-connection-types";
|
||||
@@ -1,6 +1,7 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
@@ -26,6 +27,8 @@ const authorizeDatabricksConnection = async ({
|
||||
clientSecret,
|
||||
workspaceUrl
|
||||
}: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
|
||||
await blockLocalAndPrivateIpAddresses(workspaceUrl);
|
||||
|
||||
const { data } = await request.post<TAuthorizeDatabricksConnection>(
|
||||
`${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`,
|
||||
"grant_type=client_credentials&scope=all-apis",
|
||||
|
||||
@@ -16,7 +16,7 @@ export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.obje
|
||||
workspaceUrl: z.string().trim().url().min(1, "Workspace URL required")
|
||||
});
|
||||
|
||||
export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
|
||||
const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
|
||||
.object({
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
|
||||
@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
@@ -174,20 +174,25 @@ export const authLoginServiceFactory = ({
|
||||
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||
username: email
|
||||
});
|
||||
|
||||
const serverCfg = await getServerCfg();
|
||||
|
||||
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||
throw new Error("Failed to find user");
|
||||
}
|
||||
|
||||
if (
|
||||
serverCfg.enabledLoginMethods &&
|
||||
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
|
||||
!providerAuthToken
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with email is disabled by administrator."
|
||||
});
|
||||
}
|
||||
|
||||
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||
throw new Error("Failed to find user");
|
||||
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(userEnc.userId);
|
||||
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with email is disabled by administrator."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||
@@ -573,28 +578,40 @@ export const authLoginServiceFactory = ({
|
||||
switch (authMethod) {
|
||||
case AuthMethod.GITHUB: {
|
||||
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with Github is disabled by administrator.",
|
||||
name: "Oauth 2 login"
|
||||
});
|
||||
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with Github is disabled by administrator.",
|
||||
name: "Oauth 2 login"
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AuthMethod.GOOGLE: {
|
||||
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with Google is disabled by administrator.",
|
||||
name: "Oauth 2 login"
|
||||
});
|
||||
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with Google is disabled by administrator.",
|
||||
name: "Oauth 2 login"
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AuthMethod.GITLAB: {
|
||||
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with Gitlab is disabled by administrator.",
|
||||
name: "Oauth 2 login"
|
||||
});
|
||||
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||
throw new BadRequestError({
|
||||
message: "Login with Gitlab is disabled by administrator.",
|
||||
name: "Oauth 2 login"
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -96,7 +96,9 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// special query
|
||||
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
|
||||
const findAllOrgsByUserId = async (
|
||||
userId: string
|
||||
): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => {
|
||||
try {
|
||||
const org = (await db
|
||||
.replicaNode()(TableName.OrgMembership)
|
||||
@@ -117,6 +119,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
);
|
||||
})
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole"))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
@@ -125,7 +128,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
ELSE ''
|
||||
END as "orgAuthMethod"
|
||||
`)
|
||||
)) as (TOrganizations & { orgAuthMethod: string })[];
|
||||
)) as (TOrganizations & { orgAuthMethod: string; userRole: string })[];
|
||||
|
||||
return org;
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,5 +16,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
allowSecretSharingOutsideOrganization: true,
|
||||
shouldUseNewPrivilegeSystem: true,
|
||||
privilegeUpgradeInitiatedByUsername: true,
|
||||
privilegeUpgradeInitiatedAt: true
|
||||
privilegeUpgradeInitiatedAt: true,
|
||||
bypassOrgAuthEnabled: true
|
||||
});
|
||||
|
||||
@@ -349,7 +349,8 @@ export const orgServiceFactory = ({
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
}
|
||||
}: TUpdateOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -429,7 +430,8 @@ export const orgServiceFactory = ({
|
||||
defaultMembershipRole,
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
});
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
return org;
|
||||
|
||||
@@ -73,6 +73,7 @@ export type TUpdateOrgDTO = {
|
||||
enforceMfa: boolean;
|
||||
selectedMfaMethod: MfaMethod;
|
||||
allowSecretSharingOutsideOrganization: boolean;
|
||||
bypassOrgAuthEnabled: boolean;
|
||||
}>;
|
||||
} & TOrgPermission;
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
TProjectAccessRequestDTO,
|
||||
TSearchProjectsDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TToggleProjectDeleteProtectionDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectKmsDTO,
|
||||
@@ -482,6 +483,12 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
if (project.hasDeleteProtection) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Project delete protection is enabled"
|
||||
});
|
||||
}
|
||||
|
||||
const deletedProject = await projectDAL.transaction(async (tx) => {
|
||||
// delete these so that project custom roles can be deleted in cascade effect
|
||||
// direct deletion of project without these will cause fk error
|
||||
@@ -616,6 +623,7 @@ export const projectServiceFactory = ({
|
||||
description: update.description,
|
||||
autoCapitalization: update.autoCapitalization,
|
||||
enforceCapitalization: update.autoCapitalization,
|
||||
hasDeleteProtection: update.hasDeleteProtection,
|
||||
slug: update.slug
|
||||
});
|
||||
|
||||
@@ -648,6 +656,29 @@ export const projectServiceFactory = ({
|
||||
return updatedProject;
|
||||
};
|
||||
|
||||
const toggleDeleteProtection = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
hasDeleteProtection
|
||||
}: TToggleProjectDeleteProtectionDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
const updatedProject = await projectDAL.updateById(projectId, { hasDeleteProtection });
|
||||
|
||||
return updatedProject;
|
||||
};
|
||||
|
||||
const updateVersionLimit = async ({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -1499,6 +1530,7 @@ export const projectServiceFactory = ({
|
||||
getProjectUpgradeStatus,
|
||||
getAProject,
|
||||
toggleAutoCapitalization,
|
||||
toggleDeleteProtection,
|
||||
updateName,
|
||||
upgradeProject,
|
||||
listProjectCas,
|
||||
|
||||
@@ -66,6 +66,10 @@ export type TToggleProjectAutoCapitalizationDTO = {
|
||||
autoCapitalization: boolean;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TToggleProjectDeleteProtectionDTO = {
|
||||
hasDeleteProtection: boolean;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateProjectVersionLimitDTO = {
|
||||
pitVersionLimit: number;
|
||||
workspaceSlug: string;
|
||||
@@ -86,6 +90,7 @@ export type TUpdateProjectDTO = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
autoCapitalization?: boolean;
|
||||
hasDeleteProtection?: boolean;
|
||||
slug?: string;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||
import { TServiceTokenServiceFactory } from "../service-token/service-token-service";
|
||||
|
||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||
@@ -21,6 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
||||
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
|
||||
queueService: TQueueServiceFactory;
|
||||
};
|
||||
|
||||
@@ -36,7 +38,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
secretSharingDAL,
|
||||
secretVersionV2DAL,
|
||||
identityUniversalAuthClientSecretDAL
|
||||
identityUniversalAuthClientSecretDAL,
|
||||
serviceTokenService
|
||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||
@@ -50,6 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
await secretVersionDAL.pruneExcessVersions();
|
||||
await secretVersionV2DAL.pruneExcessVersions();
|
||||
await secretFolderVersionDAL.pruneExcessVersions();
|
||||
await serviceTokenService.notifyExpiringTokens();
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||
});
|
||||
|
||||
|
||||
@@ -819,10 +819,14 @@ export const secretImportServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
);
|
||||
if (
|
||||
permission.cannot(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) return [];
|
||||
|
||||
@@ -28,5 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...stOrm, findById };
|
||||
const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => {
|
||||
try {
|
||||
const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] =
|
||||
await (tx || db.replicaNode())(TableName.ServiceToken)
|
||||
.leftJoin<TUsers>(
|
||||
TableName.Users,
|
||||
`${TableName.Users}.id`,
|
||||
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
|
||||
)
|
||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`)
|
||||
.whereRaw(
|
||||
`${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false`
|
||||
)
|
||||
.whereNotNull(`${TableName.Users}.email`)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ServiceToken),
|
||||
db.ref("name").withSchema(TableName.ServiceToken),
|
||||
db.ref("projectId").withSchema(TableName.ServiceToken),
|
||||
db.ref("createdBy").withSchema(TableName.ServiceToken),
|
||||
db.ref("email").withSchema(TableName.Users).as("createdByEmail"),
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName")
|
||||
)
|
||||
.limit(batchSize)
|
||||
.offset(offset);
|
||||
|
||||
return batch;
|
||||
} catch (err) {
|
||||
throw new DatabaseError({ error: err, name: "FindExpiredTokens" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...stOrm, findById, findExpiringTokens };
|
||||
};
|
||||
|
||||
@@ -12,11 +12,13 @@ import {
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TServiceTokenDALFactory } from "./service-token-dal";
|
||||
import {
|
||||
@@ -33,6 +35,7 @@ type TServiceTokenServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
||||
@@ -43,7 +46,8 @@ export const serviceTokenServiceFactory = ({
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
accessTokenQueue
|
||||
accessTokenQueue,
|
||||
smtpService
|
||||
}: TServiceTokenServiceFactoryDep) => {
|
||||
const createServiceToken = async ({
|
||||
iv,
|
||||
@@ -185,11 +189,56 @@ export const serviceTokenServiceFactory = ({
|
||||
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
|
||||
};
|
||||
|
||||
const notifyExpiringTokens = async () => {
|
||||
const appCfg = getConfig();
|
||||
let processedCount = 0;
|
||||
let hasMoreRecords = true;
|
||||
let offset = 0;
|
||||
const batchSize = 500;
|
||||
|
||||
while (hasMoreRecords) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset);
|
||||
|
||||
if (expiringTokens.length === 0) {
|
||||
hasMoreRecords = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
expiringTokens.map(async (token) => {
|
||||
try {
|
||||
await smtpService.sendMail({
|
||||
recipients: [token.createdByEmail],
|
||||
subjectLine: "Service Token Expiry Notice",
|
||||
template: SmtpTemplates.ServiceTokenExpired,
|
||||
substitutions: {
|
||||
tokenName: token.name,
|
||||
projectName: token.projectName,
|
||||
url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens`
|
||||
}
|
||||
});
|
||||
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true });
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to send expiration notification for token ${token.id}:`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
processedCount += expiringTokens.length;
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
return processedCount;
|
||||
};
|
||||
|
||||
return {
|
||||
createServiceToken,
|
||||
deleteServiceToken,
|
||||
getServiceToken,
|
||||
getProjectServiceTokens,
|
||||
fnValidateServiceToken
|
||||
fnValidateServiceToken,
|
||||
notifyExpiringTokens
|
||||
};
|
||||
};
|
||||
|
||||
@@ -43,7 +43,8 @@ export enum SmtpTemplates {
|
||||
SecretRequestCompleted = "secretRequestCompleted.handlebars",
|
||||
SecretRotationFailed = "secretRotationFailed.handlebars",
|
||||
ProjectAccessRequest = "projectAccess.handlebars",
|
||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars"
|
||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
||||
ServiceTokenExpired = "serviceTokenExpired.handlebars"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Service Token Expiring Soon</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Service Token Expiry Notice</h2>
|
||||
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
|
||||
|
||||
<p>This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.</p>
|
||||
|
||||
<a href="{{url}}">Create New Token</a>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user