feat(secret-rotation): implemented api and queue for secret rotation

This commit is contained in:
Akhil Mohan
2023-11-01 22:21:31 +05:30
parent 2350219cc9
commit 82e924baff
21 changed files with 1951 additions and 115 deletions
+671 -33
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -11,6 +11,7 @@
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"aws-sdk": "^2.1364.0", "aws-sdk": "^2.1364.0",
"axios": "^1.3.5", "axios": "^1.3.5",
@@ -29,12 +30,14 @@
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.2.1", "infisical-node": "^1.2.1",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"jmespath": "^0.16.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^7.4.1", "mongoose": "^7.4.1",
"mysql": "^2.18.1",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
@@ -44,6 +47,7 @@
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"pino": "^8.16.1", "pino": "^8.16.1",
"pino-http": "^8.5.1", "pino-http": "^8.5.1",
"pg": "^8.11.3",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1", "probot": "^12.3.1",
"query-string": "^7.1.3", "query-string": "^7.1.3",
@@ -98,11 +102,14 @@
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/jmespath": "^0.15.1",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/mysql": "^2.15.23",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12", "@types/passport": "^1.0.12",
"@types/pg": "^8.10.7",
"@types/picomatch": "^2.3.0", "@types/picomatch": "^2.3.0",
"@types/pino": "^7.0.5", "@types/pino": "^7.0.5",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
+5 -1
View File
@@ -10,6 +10,8 @@ import * as cloudProductsController from "./cloudProductsController";
import * as roleController from "./roleController"; import * as roleController from "./roleController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController"; import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
import * as secretApprovalRequestController from "./secretApprovalRequestsController"; import * as secretApprovalRequestController from "./secretApprovalRequestsController";
import * as secretRotationProviderController from "./secretRotationProviderController";
import * as secretRotationController from "./secretRotationController";
export { export {
secretController, secretController,
@@ -23,5 +25,7 @@ export {
cloudProductsController, cloudProductsController,
roleController, roleController,
secretApprovalPolicyController, secretApprovalPolicyController,
secretApprovalRequestController secretApprovalRequestController,
secretRotationProviderController,
secretRotationController
}; };
@@ -0,0 +1,91 @@
import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../validation/secretRotation";
import * as secretRotationService from "../../secretRotation/service";
import {
getUserProjectPermissions,
ProjectPermissionActions,
ProjectPermissionSub
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
export const createSecretRotation = async (req: Request, res: Response) => {
const {
body: {
provider,
customProvider,
interval,
outputs,
secretPath,
environment,
workspaceId,
inputs
}
} = await validateRequest(reqValidator.createSecretRotationV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
);
const secretRotation = await secretRotationService.createSecretRotation({
workspaceId,
inputs,
environment,
secretPath,
outputs,
interval,
customProvider,
provider
});
return res.send({ secretRotation });
};
export const restartSecretRotations = async (req: Request, res: Response) => {
const {
body: { id }
} = await validateRequest(reqValidator.restartSecretRotationV1, req);
const doc = await secretRotationService.getSecretRotationById({ id });
const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString());
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SecretRotation
);
const secretRotation = await secretRotationService.restartSecretRotation({ id });
return res.send({ secretRotation });
};
export const deleteSecretRotations = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.removeSecretRotationV1, req);
const doc = await secretRotationService.getSecretRotationById({ id });
const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString());
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation
);
const secretRotations = await secretRotationService.deleteSecretRotation({ id });
return res.send({ secretRotations });
};
export const getSecretRotations = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretRotationV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation
);
const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId);
return res.send({ secretRotations });
};
@@ -0,0 +1,28 @@
import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../validation/secretRotationProvider";
import * as secretRotationProviderService from "../../secretRotation/service";
import {
getUserProjectPermissions,
ProjectPermissionActions,
ProjectPermissionSub
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
export const getProviderTemplates = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.getSecretRotationProvidersV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation
);
const rotationProviderList = await secretRotationProviderService.getProviderTemplate({
workspaceId
});
return res.send(rotationProviderList);
};
+4 -3
View File
@@ -1,7 +1,8 @@
export enum ActorType { export enum ActorType {
USER = "user", USER = "user",
SERVICE = "service", SERVICE = "service",
SERVICE_V3 = "service-v3" SERVICE_V3 = "service-v3",
Machine = "machine"
} }
export enum UserAgentType { export enum UserAgentType {
+42 -47
View File
@@ -1,11 +1,5 @@
import { import { ActorType, EventType } from "./enums";
ActorType, import { IServiceTokenV3Scope, IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
EventType
} from "./enums";
import {
IServiceTokenV3Scope,
IServiceTokenV3TrustedIp
} from "../../../models/serviceTokenDataV3";
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
@@ -28,14 +22,15 @@ export interface ServiceActor {
} }
export interface ServiceActorV3 { export interface ServiceActorV3 {
type: ActorType.SERVICE_V3; type: ActorType.SERVICE_V3;
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export type Actor = export interface MachineActor {
| UserActor type: ActorType.Machine;
| ServiceActor }
| ServiceActorV3;
export type Actor = UserActor | ServiceActor | ServiceActorV3 | MachineActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@@ -226,36 +221,36 @@ interface DeleteServiceTokenEvent {
} }
interface CreateServiceTokenV3Event { interface CreateServiceTokenV3Event {
type: EventType.CREATE_SERVICE_TOKEN_V3; type: EventType.CREATE_SERVICE_TOKEN_V3;
metadata: { metadata: {
name: string; name: string;
isActive: boolean; isActive: boolean;
scopes: Array<IServiceTokenV3Scope>; scopes: Array<IServiceTokenV3Scope>;
trustedIps: Array<IServiceTokenV3TrustedIp>; trustedIps: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date; expiresAt?: Date;
} };
} }
interface UpdateServiceTokenV3Event { interface UpdateServiceTokenV3Event {
type: EventType.UPDATE_SERVICE_TOKEN_V3; type: EventType.UPDATE_SERVICE_TOKEN_V3;
metadata: { metadata: {
name?: string; name?: string;
isActive?: boolean; isActive?: boolean;
scopes?: Array<IServiceTokenV3Scope>; scopes?: Array<IServiceTokenV3Scope>;
trustedIps?: Array<IServiceTokenV3TrustedIp>; trustedIps?: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date; expiresAt?: Date;
} };
} }
interface DeleteServiceTokenV3Event { interface DeleteServiceTokenV3Event {
type: EventType.DELETE_SERVICE_TOKEN_V3; type: EventType.DELETE_SERVICE_TOKEN_V3;
metadata: { metadata: {
name: string; name: string;
isActive: boolean; isActive: boolean;
scopes: Array<IServiceTokenV3Scope>; scopes: Array<IServiceTokenV3Scope>;
expiresAt?: Date; expiresAt?: Date;
trustedIps: Array<IServiceTokenV3TrustedIp>; trustedIps: Array<IServiceTokenV3TrustedIp>;
} };
} }
interface CreateEnvironmentEvent { interface CreateEnvironmentEvent {
@@ -427,15 +422,15 @@ interface UpdateUserRole {
} }
interface UpdateUserDeniedPermissions { interface UpdateUserDeniedPermissions {
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
deniedPermissions: { deniedPermissions: {
environmentSlug: string; environmentSlug: string;
ability: string; ability: string;
}[] }[];
} };
} }
interface SecretApprovalMerge { interface SecretApprovalMerge {
type: EventType.SECRET_APPROVAL_MERGED; type: EventType.SECRET_APPROVAL_MERGED;
+5 -1
View File
@@ -10,6 +10,8 @@ import secretScanning from "./secretScanning";
import roles from "./role"; import roles from "./role";
import secretApprovalPolicy from "./secretApprovalPolicy"; import secretApprovalPolicy from "./secretApprovalPolicy";
import secretApprovalRequest from "./secretApprovalRequest"; import secretApprovalRequest from "./secretApprovalRequest";
import secretRotationProvider from "./secretRotationProvider";
import secretRotation from "./secretRotation";
export { export {
secret, secret,
@@ -23,5 +25,7 @@ export {
secretScanning, secretScanning,
roles, roles,
secretApprovalPolicy, secretApprovalPolicy,
secretApprovalRequest secretApprovalRequest,
secretRotationProvider,
secretRotation
}; };
@@ -0,0 +1,41 @@
import express from "express";
import { AuthMode } from "../../../variables";
import { requireAuth } from "../../../middleware";
import { secretRotationController } from "../../controllers/v1";
const router = express.Router();
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.createSecretRotation
);
router.post(
"/restart",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.restartSecretRotations
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.getSecretRotations
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.deleteSecretRotations
);
export default router;
@@ -0,0 +1,17 @@
import express from "express";
import { AuthMode } from "../../../variables";
import { requireAuth } from "../../../middleware";
import { secretRotationProviderController } from "../../controllers/v1";
const router = express.Router();
router.get(
"/:workspaceId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationProviderController.getProviderTemplates
);
export default router;
View File
+91
View File
@@ -0,0 +1,91 @@
import { Schema, model } from "mongoose";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
import { ISecretRotation } from "./types";
const secretRotationSchema = new Schema(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace"
},
provider: {
type: String,
required: true
},
customProvider: {
type: Schema.Types.ObjectId,
ref: "SecretRotationProvider"
},
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true
},
interval: {
type: Number,
required: true
},
lastRotatedAt: {
type: String
},
status: {
type: String,
enum: ["success", "failed"]
},
statusMessage: {
type: String
},
// encrypted data on input keys and secrets got
encryptedData: {
type: String,
select: false
},
encryptedDataIV: {
type: String,
select: false
},
encryptedDataTag: {
type: String,
select: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false,
default: ALGORITHM_AES_256_GCM
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
select: false,
default: ENCODING_SCHEME_UTF8
},
outputs: [
{
key: {
type: String,
required: true
},
secret: {
type: Schema.Types.ObjectId,
ref: "Secret"
}
}
]
},
{
timestamps: true
}
);
export const SecretRotation = model<ISecretRotation>("SecretRotation", secretRotationSchema);
@@ -0,0 +1,215 @@
import {
ISecretRotationProviderTemplate,
TProviderFunctionTypes,
TDbProviderClients,
TAssignOp
} from "./types";
const SENDGRID_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_api_key: { type: "string" as const },
scopes: { type: "array", items: { type: "string" as const } }
},
required: ["admin_api_key", "scopes"],
additionalProperties: false
},
outputs: {
api_key: { type: "string" }
},
internal: {
api_key_id: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys",
method: "POST",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
body: {
name: "infisical-${random | 16}",
scopes: { ref: "inputs.scopes" }
},
setter: {
"outputs.api_key": {
assign: TAssignOp.JmesPath as const,
path: "api_key"
},
"internal.api_key_id": {
assign: TAssignOp.JmesPath as const,
path: "api_key_id"
}
}
},
remove: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "DELETE"
},
test: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "GET"
}
}
};
const POSTGRES_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const },
port: { type: "integer" as const, default: "5432" },
username1: { type: "string", default: "infisical-pg-user1" },
username2: { type: "string", default: "infisical-pg-user2" }
},
required: ["admin_username", "admin_password", "host", "database"],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "SELECT NOW()"
}
}
};
const MYSQL_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const },
port: { type: "integer" as const, default: "3306" },
username1: { type: "string", default: "infisical-sql-user1" },
username2: { type: "string", default: "infisical-sql-user2" }
},
required: ["admin_username", "admin_password", "host", "database"],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query:
"ALTER USER ${internal.username} IDENTIFIED WITH mysql_native_password BY '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
query: "SELECT NOW()"
}
}
};
export const providerRotationTemplates: ISecretRotationProviderTemplate[] = [
{
name: "sendgrid",
title: "Twilio Sendgrid",
image: "sendgrid.png",
description: "Rotate Twilio Sendgrid API keys",
template: SENDGRID_TEMPLATE
},
{
name: "postgres",
title: "PostgreSQL",
image: "postgres.png",
description: "Rotate PostgreSQL/CockroachDB user credentials",
template: POSTGRES_TEMPLATE
},
{
name: "mysql",
title: "MySQL",
image: "mysql.png",
description: "Rotate MySQL@7/MariaDB user credentials",
template: MYSQL_TEMPLATE
}
];
+417
View File
@@ -0,0 +1,417 @@
import axios from "axios";
import Queue, { Job } from "bull";
import jmespath from "jmespath";
import { customAlphabet } from "nanoid";
import { Client as PgClient } from "pg";
import mysql from "mysql";
import { client, getRootEncryptionKey } from "../../config";
import { BotService, TelemetryService } from "../../services";
import { SecretRotation } from "./models";
import { providerRotationTemplates } from "./providerTemplates";
import {
ISecretRotationData,
ISecretRotationEncData,
ISecretRotationProviderTemplate,
TAssignOp,
TDbProviderClients,
TDbProviderFunction,
TDirectAssignOp,
THttpProviderFunction,
TProviderFunction,
TProviderFunctionTypes
} from "./types";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { ISecret, Secret } from "../../models";
import { SECRET_SHARED } from "../../variables";
import { EESecretService } from "../services";
import { SecretVersion } from "../models";
const REGEX = /\${([^}]+)}/g;
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string);
const interpolate = (data: any, getValue: (key: string) => unknown) => {
if (!data) return;
if (typeof data === "number") return data;
if (typeof data === "string") {
return data.replace(REGEX, (_a, b) => getValue(b) as string);
}
if (typeof data === "object" && Array.isArray(data)) {
data.forEach((el, index) => {
data[index] = interpolate(el, getValue);
});
}
if (typeof data === "object") {
if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref);
const temp = data as Record<string, unknown>; // for converting ts object to record type
Object.keys(temp).forEach((key) => {
temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue);
});
}
return data;
};
const getInterpolationValue = (variables: ISecretRotationData) => (key: string) => {
if (key.includes("|")) {
const [keyword, ...arg] = key.split("|").map((el) => el.trim());
switch (keyword) {
case "random": {
return nanoId(parseInt(arg[0], 10));
}
default: {
throw Error(`Interpolation key not found - ${key}`);
}
}
}
const [type, keyName] = key.split(".").map((el) => el.trim());
return variables[type as keyof ISecretRotationData][keyName];
};
const secretRotationHttpFn = async (
func: THttpProviderFunction,
variables: ISecretRotationData
) => {
// string interpolation
const headers = interpolate(func.header, getInterpolationValue(variables));
const url = interpolate(func.url, getInterpolationValue(variables));
const body = interpolate(func.body, getInterpolationValue(variables));
// axios will automatically throw error if req status is not between 2xx range
return axios({ method: func.method, url, headers, data: body });
};
const secretRotationDbFn = async (func: TDbProviderFunction, variables: ISecretRotationData) => {
const { type, client, pre, ...dbConnection } = func;
const { username, password, host, database, port, query } = interpolate(
dbConnection,
getInterpolationValue(variables)
);
if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host");
if (client === TDbProviderClients.Pg) {
const pgClient = new PgClient({ user: username, password, host, database, port });
await pgClient.connect();
const res = await pgClient.query(query);
await pgClient.end();
return res.rows[0];
} else if (client === TDbProviderClients.Sql) {
const sqlClient = mysql.createPool({
user: username,
password,
host,
database,
port,
connectionLimit: 1
});
const res = await new Promise((resolve, reject) => {
sqlClient.query(query, (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
await new Promise((resolve, reject) => {
sqlClient.end(function (err) {
if (err) return reject(err);
return resolve({});
});
});
return (res as any)?.[0];
}
};
const secretRotationPreSetFn = (
op: Record<string, TDirectAssignOp>,
variables: ISecretRotationData
) => {
const getValFn = getInterpolationValue(variables);
Object.entries(op || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
variables[type][keyName] = interpolate(assignFn.value, getValFn);
});
};
const secretRotationSetFn = async (func: TProviderFunction, variables: ISecretRotationData) => {
const getValFn = getInterpolationValue(variables);
// http setter
if (func.type === TProviderFunctionTypes.HTTP) {
const res = await secretRotationHttpFn(func, variables);
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
if (assignFn.assign === TAssignOp.JmesPath) {
variables[type][keyName] = jmespath.search(res.data, assignFn.path);
} else if (assignFn.value) {
variables[type][keyName] = interpolate(assignFn.value, getValFn);
}
});
// db setter
} else if (func.type === TProviderFunctionTypes.DB) {
const data = await secretRotationDbFn(func, variables);
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
if (assignFn.assign === TAssignOp.JmesPath) {
if (typeof data === "object") {
variables[type][keyName] = jmespath.search(data, assignFn.path);
}
} else if (assignFn.value) {
variables[type][keyName] = interpolate(assignFn.value, getValFn);
}
});
}
};
const secretRotationTestFn = async (func: TProviderFunction, variables: ISecretRotationData) => {
if (func.type === TProviderFunctionTypes.HTTP) {
await secretRotationHttpFn(func, variables);
} else if (func.type === TProviderFunctionTypes.DB) {
await secretRotationDbFn(func, variables);
}
};
const secretRotationRemoveFn = async (func: TProviderFunction, variables: ISecretRotationData) => {
if (!func) return;
if (func.type === TProviderFunctionTypes.HTTP) {
// string interpolation
return await secretRotationHttpFn(func, variables);
}
};
secretRotationQueue.process(async (job: Job) => {
const rotationStratDocId = job.data.rotationDocId;
const secretRotation = await SecretRotation.findById(rotationStratDocId)
.select("+encryptedData +encryptedDataTag +encryptedDataIV")
.populate<{
outputs: [
{
key: string;
secret: ISecret;
}
];
}>("outputs.secret");
const infisicalRotationProvider = providerRotationTemplates.find(
({ name }) => name === secretRotation?.provider
);
try {
if (!infisicalRotationProvider || !secretRotation)
throw new Error("Failed to find rotation strategy");
if (secretRotation.outputs.some(({ secret }) => !secret))
throw new Error("Secrets not found in dashboard");
const workspaceId = secretRotation.workspace;
// deep copy
const provider = JSON.parse(
JSON.stringify(infisicalRotationProvider)
) as ISecretRotationProviderTemplate;
// decrypt user provided inputs for secret rotation
const rootEncryptionKey = await getRootEncryptionKey();
const decryptedData = client.decryptSymmetric(
secretRotation.encryptedData,
rootEncryptionKey,
secretRotation.encryptedDataIV,
secretRotation.encryptedDataTag
);
const variables = JSON.parse(decryptedData) as ISecretRotationEncData;
// rotation set cycle
const newCredential: ISecretRotationData = {
inputs: variables.inputs,
outputs: {},
internal: {}
};
// special glue code for database
if (provider.template.functions.set.type === TProviderFunctionTypes.DB) {
const lastCred = variables.creds.at(-1);
if (lastCred && variables.creds.length === 1) {
newCredential.internal.username =
lastCred.internal.username === variables.inputs.username1
? variables.inputs.username2
: variables.inputs.username1;
} else {
newCredential.internal.username = lastCred
? lastCred.internal.username
: variables.inputs.username1;
}
}
if (provider.template.functions.set?.pre) {
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
}
await secretRotationSetFn(provider.template.functions.set, newCredential);
await secretRotationTestFn(provider.template.functions.test, newCredential);
if (variables.creds.length === 2) {
const deleteCycleCred = variables.creds.pop();
if (deleteCycleCred && provider.template.functions.remove) {
const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred };
await secretRotationRemoveFn(provider.template.functions.remove, deleteCycleVar);
}
}
variables.creds.unshift({ outputs: newCredential.outputs, internal: newCredential.internal });
const { ciphertext, iv, tag } = client.encryptSymmetric(
JSON.stringify(variables),
rootEncryptionKey
);
// save the rotation state
await SecretRotation.findByIdAndUpdate(rotationStratDocId, {
encryptedData: ciphertext,
encryptedDataIV: iv,
encryptedDataTag: tag,
status: "success",
statusMessage: "Rotated successfully",
lastRotatedAt: new Date().toUTCString()
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: secretRotation.workspace
});
const encryptedSecrets = secretRotation.outputs.map(({ key: outputKey, secret }) => ({
secret,
value: encryptSymmetric128BitHexKeyUTF8({
plaintext:
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey]),
key
})
}));
// now save the secret do a bulk update
// can't use the updateSecret function due to various parameter required issue
// REFACTOR(akhilmhdh): secret module should be lot more flexible. Ability to update bulk or individually by blindIndex, by id etc
await Secret.bulkWrite(
encryptedSecrets.map(({ secret, value }) => ({
updateOne: {
filter: {
workspace: workspaceId,
environment: secretRotation.environment,
_id: secret._id,
type: SECRET_SHARED
},
update: {
$inc: {
version: 1
},
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag
}
}
}))
);
await EESecretService.addSecretVersions({
secretVersions: encryptedSecrets.map(({ secret, value }) => {
const {
_id,
version,
workspace,
type,
folder,
secretBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding,
environment,
algorithm,
keyEncoding
} = secret;
return new SecretVersion({
secret: _id,
version: version + 1,
workspace: workspace,
type,
folder,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex,
secretKeyCiphertext: secretKeyCiphertext,
secretKeyIV: secretKeyIV,
secretKeyTag: secretKeyTag,
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag,
algorithm,
keyEncoding,
skipMultilineEncoding
});
})
});
// akhilmhdh: @tony need to do something about this as its depend on authData which is not possibile in here
// await EEAuditLogService.createAuditLog(
// {actor:ActorType.Machine},
// {
// type: EventType.UPDATE_SECRETS,
// metadata: {
// environment,
// secretPath,
// secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
// secretId: _id.toString(),
// secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
// secretVersion: version + 1
// }))
// }
// },
// {
// workspaceId
// }
// );
const folderId = encryptedSecrets?.[0]?.secret?.folder;
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment: secretRotation.environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets rotated",
properties: {
numberOfSecrets: encryptedSecrets.length,
environment: secretRotation.environment,
workspaceId,
folderId
}
});
}
} catch (err) {
console.error(err);
await SecretRotation.findByIdAndUpdate(rotationStratDocId, {
status: "failed",
statusMessage: (err as Error).message,
lastRotatedAt: new Date().toUTCString()
});
}
return Promise.resolve();
});
const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const startSecretRotationQueue = async (rotationDocId: string, interval: number) => {
// when migration to bull mq just use the option immedite to trigger repeatable immediately
secretRotationQueue.add({ rotationDocId }, { jobId: rotationDocId, removeOnComplete: true });
return secretRotationQueue.add(
{ rotationDocId },
{ repeat: { every: daysToMillisecond(interval) }, jobId: rotationDocId }
);
};
export const removeSecretRotationQueue = async (rotationDocId: string, interval: number) => {
return secretRotationQueue.removeRepeatable({ every: interval * 1000, jobId: rotationDocId });
};
+116
View File
@@ -0,0 +1,116 @@
import {
ISecretRotationEncData,
ISecretRotationProviderTemplate,
TCreateSecretRotation,
TGetProviderTemplates
} from "./types";
import {
providerRotationTemplates as infisicalRotationTemplates,
providerRotationTemplates
} from "./providerTemplates";
import { SecretRotation } from "./models";
import { client, getRootEncryptionKey } from "../../config";
import { BadRequestError } from "../../utils/errors";
import Ajv from "ajv";
import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue";
const ajv = new Ajv();
export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => {
return {
custom: [],
providers: infisicalRotationTemplates
};
};
export const createSecretRotation = async ({
workspaceId,
secretPath,
environment,
provider,
interval,
inputs,
outputs
}: TCreateSecretRotation) => {
const rotationTemplate = providerRotationTemplates.find(({ name }) => name === provider);
if (!rotationTemplate) throw BadRequestError({ message: "Provider not found" });
const formattedInputs: Record<string, unknown> = {};
Object.entries(inputs).forEach(([key, value]) => {
const type = rotationTemplate.template.inputs.properties[key].type;
if (type === "string") {
formattedInputs[key] = value;
return;
}
if (type === "integer") {
formattedInputs[key] = parseInt(value as string, 10);
return;
}
formattedInputs[key] = JSON.parse(value as string);
});
// ensure input one follows the correct schema
const valid = ajv.validate(rotationTemplate.template.inputs, formattedInputs);
if (!valid) {
throw BadRequestError({ message: ajv.errors?.[0].message });
}
const encData: Partial<ISecretRotationEncData> = {
inputs: formattedInputs,
creds: []
};
const rootEncryptionKey = await getRootEncryptionKey();
const { ciphertext, iv, tag } = client.encryptSymmetric(
JSON.stringify(encData),
rootEncryptionKey
);
const secretRotation = new SecretRotation({
workspace: workspaceId,
provider,
environment,
secretPath,
interval,
outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret })),
encryptedData: ciphertext,
encryptedDataIV: iv,
encryptedDataTag: tag
});
await secretRotation.save();
await startSecretRotationQueue(secretRotation._id.toString(), interval);
return secretRotation;
};
export const deleteSecretRotation = async ({ id }: { id: string }) => {
const doc = await SecretRotation.findByIdAndRemove(id);
if (!doc) throw BadRequestError({ message: "Rotation not found" });
await removeSecretRotationQueue(doc._id.toString(), doc.interval);
return doc;
};
export const restartSecretRotation = async ({ id }: { id: string }) => {
const secretRotation = await SecretRotation.findById(id);
if (!secretRotation) throw BadRequestError({ message: "Rotation not found" });
await removeSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval);
await startSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval);
return secretRotation;
};
export const getSecretRotationById = async ({ id }: { id: string }) => {
const doc = await SecretRotation.findById(id);
if (!doc) throw BadRequestError({ message: "Rotation not found" });
return doc;
};
export const getSecretRotationOfWorkspace = async (workspaceId: string) => {
const secretRotations = await SecretRotation.find({
workspace: workspaceId
}).populate("outputs.secret");
return secretRotations;
};
+131
View File
@@ -0,0 +1,131 @@
import { Document, Types } from "mongoose";
export interface ISecretRotation extends Document {
_id: Types.ObjectId;
name: string;
interval: number;
provider: string;
customProvider: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
secretPath: string;
outputs: Array<{
key: string;
secret: Types.ObjectId;
}>;
status?: "success" | "failed";
lastRotatedAt?: string;
statusMessage?: string;
encryptedData: string;
encryptedDataIV: string;
encryptedDataTag: string;
algorithm: string;
keyEncoding: string;
}
export type ISecretRotationEncData = {
inputs: Record<string, unknown>;
creds: Array<{
outputs: Record<string, unknown>;
internal: Record<string, unknown>;
}>;
};
export type ISecretRotationData = {
inputs: Record<string, unknown>;
outputs: Record<string, unknown>;
internal: Record<string, unknown>;
};
export type ISecretRotationProviderTemplate = {
name: string;
title: string;
image?: string;
description?: string;
template: TProviderTemplate;
};
export enum TProviderFunctionTypes {
HTTP = "http",
DB = "database"
}
export enum TDbProviderClients {
// postgres, cockroack db, amazon red shift
Pg = "pg",
// mysql and maria db
Sql = "sql"
}
export enum TAssignOp {
Direct = "direct",
JmesPath = "jmesopath"
}
export type TJmesPathAssignOp = {
assign: TAssignOp.JmesPath;
path: string;
};
export type TDirectAssignOp = {
assign: TAssignOp.Direct;
value: string;
};
export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp;
export type THttpProviderFunction = {
type: TProviderFunctionTypes.HTTP;
url: string;
method: string;
header?: Record<string, string>;
query?: Record<string, string>;
body?: Record<string, unknown>;
setter?: Record<string, TAssignFunction>;
pre?: Record<string, TDirectAssignOp>;
};
export type TDbProviderFunction = {
type: TProviderFunctionTypes.DB;
client: TDbProviderClients;
username: string;
password: string;
host: string;
database: string;
port: string;
query: string;
setter?: Record<string, TAssignFunction>;
pre?: Record<string, TDirectAssignOp>;
};
export type TProviderFunction = THttpProviderFunction | TDbProviderFunction;
export type TProviderTemplate = {
inputs: {
type: "object";
properties: Record<string, { type: string; [x: string]: unknown }>;
required?: string[];
};
outputs: Record<string, unknown>;
functions: {
set: TProviderFunction;
remove?: TProviderFunction;
test: TProviderFunction;
};
};
// function type args
export type TGetProviderTemplates = {
workspaceId: string;
};
export type TCreateSecretRotation = {
provider: string;
customProvider?: string;
workspaceId: string;
secretPath: string;
environment: string;
interval: number;
inputs: Record<string, unknown>;
outputs: Record<string, string>;
};
+10 -1
View File
@@ -50,7 +50,8 @@ export enum ProjectPermissionSub {
Workspace = "workspace", Workspace = "workspace",
Secrets = "secrets", Secrets = "secrets",
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval" SecretApproval = "secret-approval",
SecretRotation = "secret-rotation"
} }
type SubjectFields = { type SubjectFields = {
@@ -74,6 +75,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -92,6 +94,11 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
@@ -162,6 +169,7 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
@@ -214,6 +222,7 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
@@ -0,0 +1,32 @@
import { z } from "zod";
export const createSecretRotationV1 = z.object({
body: z.object({
workspaceId: z.string().trim(),
secretPath: z.string().trim(),
environment: z.string().trim(),
interval: z.number().min(1),
provider: z.string().trim(),
customProvider: z.string().trim().optional(),
inputs: z.record(z.unknown()),
outputs: z.record(z.string())
})
});
export const restartSecretRotationV1 = z.object({
body: z.object({
id: z.string().trim()
})
});
export const getSecretRotationV1 = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const removeSecretRotationV1 = z.object({
params: z.object({
id: z.string().trim()
})
});
@@ -0,0 +1,7 @@
import { z } from "zod";
export const getSecretRotationProvidersV1 = z.object({
params: z.object({
workspaceId: z.string()
})
});
+8 -4
View File
@@ -27,8 +27,10 @@ import {
users as eeUsersRouter, users as eeUsersRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
roles as v1RoleRouter, roles as v1RoleRouter,
secretApprovalPolicy as v1SecretApprovalPolicy, secretApprovalPolicy as v1SecretApprovalPolicyRouter,
secretApprovalRequest as v1SecretApprovalRequest, secretApprovalRequest as v1SecretApprovalRequestRouter,
secretRotation as v1SecretRotation,
secretRotationProvider as v1SecretRotationProviderRouter,
secretScanning as v1SecretScanningRouter secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1"; } from "./ee/routes/v1";
import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3"; import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3";
@@ -194,6 +196,8 @@ const main = async () => {
app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter);
app.use("/api/v3/api-key", v3apiKeyDataRouter); // new app.use("/api/v3/api-key", v3apiKeyDataRouter); // new
app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new
app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter);
app.use("/api/v1/secret-rotations", v1SecretRotation);
// v1 routes // v1 routes
app.use("/api/v1/signup", v1SignupRouter); app.use("/api/v1/signup", v1SignupRouter);
@@ -217,9 +221,9 @@ const main = async () => {
app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImpsRouter); app.use("/api/v1/secret-imports", v1SecretImpsRouter);
app.use("/api/v1/roles", v1RoleRouter); app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy); app.use("/api/v1/secret-approvals", v1SecretApprovalPolicyRouter);
app.use("/api/v1/sso", v1SSORouter); app.use("/api/v1/sso", v1SSORouter);
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest); app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequestRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
+13 -25
View File
@@ -1,39 +1,27 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { IServiceTokenData, IServiceTokenDataV3, IUser } from "../../models";
IServiceTokenData, import { ServiceActor, ServiceActorV3, UserActor, UserAgentType } from "../../ee/models";
IServiceTokenDataV3,
IUser,
} from "../../models";
import {
ServiceActor,
ServiceActorV3,
UserActor,
UserAgentType
} from "../../ee/models";
interface BaseAuthData { interface BaseAuthData {
ipAddress: string; ipAddress: string;
userAgent: string; userAgent: string;
userAgentType: UserAgentType; userAgentType: UserAgentType;
tokenVersionId?: Types.ObjectId; tokenVersionId?: Types.ObjectId;
} }
export interface UserAuthData extends BaseAuthData { export interface UserAuthData extends BaseAuthData {
actor: UserActor; actor: UserActor;
authPayload: IUser; authPayload: IUser;
} }
export interface ServiceTokenV3AuthData extends BaseAuthData { export interface ServiceTokenV3AuthData extends BaseAuthData {
actor: ServiceActorV3; actor: ServiceActorV3;
authPayload: IServiceTokenDataV3; authPayload: IServiceTokenDataV3;
} }
export interface ServiceTokenAuthData extends BaseAuthData { export interface ServiceTokenAuthData extends BaseAuthData {
actor: ServiceActor; actor: ServiceActor;
authPayload: IServiceTokenData; authPayload: IServiceTokenData;
} }
export type AuthData = export type AuthData = UserAuthData | ServiceTokenV3AuthData | ServiceTokenAuthData;
| UserAuthData
| ServiceTokenV3AuthData
| ServiceTokenAuthData;