mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Continue refactoring remaining middleware to be compatible with multiple clients
This commit is contained in:
@@ -15,32 +15,28 @@ import {
|
|||||||
const requireSecretSnapshotAuth = ({
|
const requireSecretSnapshotAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
const { secretSnapshotId } = req.params;
|
||||||
const { secretSnapshotId } = req.params;
|
|
||||||
|
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
||||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
|
||||||
|
if (!secretSnapshot) {
|
||||||
if (!secretSnapshot) {
|
return next(SecretSnapshotNotFoundError({
|
||||||
return next(SecretSnapshotNotFoundError({
|
message: 'Failed to find secret snapshot'
|
||||||
message: 'Failed to find secret snapshot'
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
await validateMembership({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId: secretSnapshot.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
req.secretSnapshot = secretSnapshot as any;
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (err) {
|
|
||||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id,
|
||||||
|
workspaceId: secretSnapshot.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
req.secretSnapshot = secretSnapshot as any;
|
||||||
|
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
BotKey,
|
BotKey,
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
IUser
|
IUser,
|
||||||
|
User,
|
||||||
|
IServiceAccount,
|
||||||
|
ServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
ServiceTokenData
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
generateKeyPair,
|
generateKeyPair,
|
||||||
@@ -12,8 +18,88 @@ import {
|
|||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
decryptAsymmetric
|
decryptAsymmetric
|
||||||
} from '../utils/crypto';
|
} from '../utils/crypto';
|
||||||
import { SECRET_SHARED } from '../variables';
|
import {
|
||||||
|
SECRET_SHARED,
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
import { getEncryptionKey } from '../config';
|
import { getEncryptionKey } from '../config';
|
||||||
|
import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
import {
|
||||||
|
validateMembership
|
||||||
|
} from '../helpers/membership';
|
||||||
|
import {
|
||||||
|
validateUserClientForWorkspace
|
||||||
|
} from '../helpers/user';
|
||||||
|
import {
|
||||||
|
validateServiceAccountClientForWorkspace
|
||||||
|
} from '../helpers/serviceAccount';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for bot with id [botId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.botId - id of bot to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
|
*/
|
||||||
|
const validateClientForBot = async ({
|
||||||
|
authData,
|
||||||
|
botId,
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
botId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
}) => {
|
||||||
|
const bot = await Bot.findById(botId);
|
||||||
|
|
||||||
|
if (!bot) throw BotNotFoundError();
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service token authorization for bot'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: bot.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw BotNotFoundError({
|
||||||
|
message: 'Failed client authorization for bot'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
@@ -222,6 +308,7 @@ const decryptSymmetricHelper = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForBot,
|
||||||
createBot,
|
createBot,
|
||||||
getSecretsHelper,
|
getSecretsHelper,
|
||||||
encryptSymmetricHelper,
|
encryptSymmetricHelper,
|
||||||
|
|||||||
@@ -1,10 +1,106 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Membership, Key } from '../models';
|
import {
|
||||||
|
Membership,
|
||||||
|
Key,
|
||||||
|
IUser,
|
||||||
|
User,
|
||||||
|
IServiceAccount,
|
||||||
|
ServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
MembershipNotFoundError,
|
MembershipNotFoundError,
|
||||||
BadRequestError
|
BadRequestError,
|
||||||
|
UnauthorizedRequestError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
import {
|
||||||
|
validateUserClientForWorkspace
|
||||||
|
} from '../helpers/user';
|
||||||
|
import {
|
||||||
|
validateServiceAccountClientForWorkspace
|
||||||
|
} from '../helpers/serviceAccount';
|
||||||
|
import {
|
||||||
|
validateServiceTokenDataClientForWorkspace
|
||||||
|
} from '../helpers/serviceTokenData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for membership with id [membershipId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
|
||||||
|
* @returns {Membership} - validated membership
|
||||||
|
*/
|
||||||
|
const validateClientForMembership = async ({
|
||||||
|
authData,
|
||||||
|
membershipId,
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
membershipId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const membership = await Membership.findById(membershipId);
|
||||||
|
|
||||||
|
if (!membership) throw MembershipNotFoundError({
|
||||||
|
message: 'Failed to find membership'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
await validateServiceAccountClientForWorkspace({
|
||||||
|
serviceAccount: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
await validateServiceTokenDataClientForWorkspace({
|
||||||
|
serviceTokenData: authData.authPayload,
|
||||||
|
workspaceId: new Types.ObjectId(membership.workspace)
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateUserClientForWorkspace({
|
||||||
|
user: authData.authPayload,
|
||||||
|
workspaceId: membership.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
@@ -21,7 +117,7 @@ const validateMembership = async ({
|
|||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId;
|
userId: Types.ObjectId;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
acceptedRoles?: string[];
|
acceptedRoles?: Array<'admin' | 'member'>;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
const membership = await Membership.findOne({
|
||||||
@@ -134,6 +230,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForMembership,
|
||||||
validateMembership,
|
validateMembership,
|
||||||
addMemberships,
|
addMemberships,
|
||||||
findMembership,
|
findMembership,
|
||||||
|
|||||||
@@ -1,10 +1,98 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { MembershipOrg, Workspace, Membership, Key } from '../models';
|
import {
|
||||||
|
MembershipOrg,
|
||||||
|
Workspace,
|
||||||
|
Membership,
|
||||||
|
Key,
|
||||||
|
IUser,
|
||||||
|
User,
|
||||||
|
IServiceAccount,
|
||||||
|
ServiceAccount,
|
||||||
|
IServiceTokenData,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
MembershipOrgNotFoundError,
|
MembershipOrgNotFoundError,
|
||||||
BadRequestError
|
BadRequestError,
|
||||||
|
UnauthorizedRequestError
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate authenticated clients for organization membership with id [membershipOrgId] based
|
||||||
|
* on any known permissions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.authData - authenticated client details
|
||||||
|
* @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against
|
||||||
|
* @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles
|
||||||
|
* @param {MembershipOrg} - validated organization membership
|
||||||
|
*/
|
||||||
|
const validateClientForMembershipOrg = async ({
|
||||||
|
authData,
|
||||||
|
membershipOrgId,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
authData: {
|
||||||
|
authMode: string;
|
||||||
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
|
};
|
||||||
|
membershipOrgId: Types.ObjectId;
|
||||||
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
|
}) => {
|
||||||
|
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
|
||||||
|
|
||||||
|
if (!membershipOrg) throw MembershipOrgNotFoundError({
|
||||||
|
message: 'Failed to find organization membership '
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
|
await validateMembershipOrg({
|
||||||
|
userId: authData.authPayload._id,
|
||||||
|
organizationId: membershipOrg.organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||||
|
if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service account client authorization for organization membership'
|
||||||
|
});
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service account client authorization for organization membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||||
|
await validateMembershipOrg({
|
||||||
|
userId: authData.authPayload._id,
|
||||||
|
organizationId: membershipOrg.organization,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
return membershipOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed client authorization for organization membership'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
||||||
@@ -22,8 +110,8 @@ const validateMembershipOrg = async ({
|
|||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId;
|
userId: Types.ObjectId;
|
||||||
organizationId: Types.ObjectId;
|
organizationId: Types.ObjectId;
|
||||||
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
|
||||||
acceptedStatuses: Array<'invited' | 'accepted'>;
|
acceptedStatuses?: Array<'invited' | 'accepted'>;
|
||||||
}) => {
|
}) => {
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: userId,
|
user: userId,
|
||||||
@@ -34,12 +122,16 @@ const validateMembershipOrg = async ({
|
|||||||
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
|
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
if (acceptedRoles) {
|
||||||
throw BadRequestError({ message: 'Failed to validate organization membership role' });
|
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||||
|
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
if (acceptedStatuses) {
|
||||||
throw BadRequestError({ message: 'Failed to validate organization membership status' });
|
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||||
|
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return membershipOrg;
|
return membershipOrg;
|
||||||
@@ -164,6 +256,7 @@ const deleteMembershipOrg = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateClientForMembershipOrg,
|
||||||
validateMembershipOrg,
|
validateMembershipOrg,
|
||||||
findMembershipOrg,
|
findMembershipOrg,
|
||||||
addMembershipsOrg,
|
addMembershipsOrg,
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const validateClientForOrganization = async ({
|
|||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: 'Failed service token authorization for organization resource'
|
message: 'Failed service token authorization for organization'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ const validateClientForOrganization = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: 'Failed client authorization for organization resource'
|
message: 'Failed client authorization for organization'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
IServiceTokenData,
|
IServiceTokenData,
|
||||||
ISecret,
|
ISecret,
|
||||||
IOrganization,
|
IOrganization,
|
||||||
|
IServiceAccountWorkspacePermission,
|
||||||
ServiceAccountWorkspacePermission
|
ServiceAccountWorkspacePermission
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
@@ -111,16 +112,19 @@ const validateClientForServiceAccount = async ({
|
|||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
if (environment) {
|
if (environment) {
|
||||||
|
// case: environment specified ->
|
||||||
|
// evaluate service account authorization for workspace
|
||||||
|
// in the context of a specific environment [environment]
|
||||||
const permission = await ServiceAccountWorkspacePermission.findOne({
|
const permission = await ServiceAccountWorkspacePermission.findOne({
|
||||||
serviceAccount,
|
serviceAccount,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!permission) throw UnauthorizedRequestError({
|
if (!permission) throw UnauthorizedRequestError({
|
||||||
message: 'Failed service account authorization for the given workspace environment'
|
message: 'Failed service account authorization for the given workspace environment'
|
||||||
});
|
});
|
||||||
|
|
||||||
let runningIsDisallowed = false;
|
let runningIsDisallowed = false;
|
||||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||||
switch (requiredPermission) {
|
switch (requiredPermission) {
|
||||||
@@ -140,6 +144,20 @@ const validateClientForServiceAccount = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// case: no environment specified ->
|
||||||
|
// evaluate service account authorization for workspace
|
||||||
|
// without need of environment [environment]
|
||||||
|
|
||||||
|
const permission = await ServiceAccountWorkspacePermission.findOne({
|
||||||
|
serviceAccount,
|
||||||
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!permission) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed service account authorization for the given workspace'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,20 +34,24 @@ import { UnauthorizedRequestError } from '../utils/errors';
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceTokenData.environment !== environment) {
|
if (environment) {
|
||||||
// case: invalid environment passed
|
// case: environment is specified
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: 'Failed service token authorization for the given workspace environment'
|
if (serviceTokenData.environment !== environment) {
|
||||||
});
|
// case: invalid environment passed
|
||||||
}
|
|
||||||
|
|
||||||
requiredPermissions?.forEach((permission) => {
|
|
||||||
if (!serviceTokenData.permissions.includes(permission)) {
|
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
message: 'Failed service token authorization for the given workspace environment'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
requiredPermissions?.forEach((permission) => {
|
||||||
|
if (!serviceTokenData.permissions.includes(permission)) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -179,21 +179,23 @@ const validateUserClientForWorkspace = async ({
|
|||||||
user,
|
user,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
}: {
|
}: {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// validate user membership in workspace
|
// validate user membership in workspace
|
||||||
const membership = await validateMembership({
|
const membership = await validateMembership({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
acceptedRoles
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: refactor
|
|
||||||
let runningIsDisallowed = false;
|
let runningIsDisallowed = false;
|
||||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||||
switch (requiredPermission) {
|
switch (requiredPermission) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { validateUserClientForWorkspace } from '../helpers/user';
|
|||||||
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
|
||||||
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
|
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
|
||||||
import { validateMembership } from '../helpers/membership';
|
import { validateMembership } from '../helpers/membership';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
@@ -34,28 +34,38 @@ import {
|
|||||||
* @param {Object} obj.authData - authenticated client details
|
* @param {Object} obj.authData - authenticated client details
|
||||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
|
||||||
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
* @param {String} obj.environment - (optional) environment in workspace to validate against
|
||||||
|
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||||
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
|
||||||
*/
|
*/
|
||||||
const validateClientForWorkspace = async ({
|
const validateClientForWorkspace = async ({
|
||||||
authData,
|
authData,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: {
|
||||||
authMode: string;
|
authMode: string;
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||||
},
|
};
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
|
const workspace = await Workspace.findById(workspaceId);
|
||||||
|
|
||||||
|
if (!workspace) throw WorkspaceNotFoundError({
|
||||||
|
message: 'Failed to find workspace'
|
||||||
|
});
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
const membership = await validateUserClientForWorkspace({
|
const membership = await validateUserClientForWorkspace({
|
||||||
user: authData.authPayload,
|
user: authData.authPayload,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,6 +99,7 @@ const validateClientForWorkspace = async ({
|
|||||||
user: authData.authPayload,
|
user: authData.authPayload,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,7 +107,7 @@ const validateClientForWorkspace = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: 'Failed client authorization for workspace resource'
|
message: 'Failed client authorization for workspace'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { AccountNotFoundError } from '../utils/errors';
|
|||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
// TODO: transform
|
||||||
|
|
||||||
const requireBotAuth = ({
|
const requireBotAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
location = 'params'
|
location = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
location?: req;
|
location?: req;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err
|
|||||||
const requireIntegrationAuth = ({
|
const requireIntegrationAuth = ({
|
||||||
acceptedRoles
|
acceptedRoles
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
// integration authorization middleware
|
// integration authorization middleware
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const requireIntegrationAuthorizationAuth = ({
|
|||||||
attachAccessToken = true,
|
attachAccessToken = true,
|
||||||
location = 'params'
|
location = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
attachAccessToken?: boolean;
|
attachAccessToken?: boolean;
|
||||||
location?: req;
|
location?: req;
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
Membership,
|
Membership,
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { validateMembership } from '../helpers/membership';
|
import {
|
||||||
|
validateClientForMembership,
|
||||||
|
validateMembership
|
||||||
|
} from '../helpers/membership';
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
@@ -16,43 +20,25 @@ type req = 'params' | 'body' | 'query';
|
|||||||
*/
|
*/
|
||||||
const requireMembershipAuth = ({
|
const requireMembershipAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
location = 'params'
|
locationMembershipId = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
location?: req;
|
locationMembershipId: req
|
||||||
}) => {
|
}) => {
|
||||||
return async (
|
return async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
try {
|
const { membershipId } = req[locationMembershipId];
|
||||||
const { membershipId } = req[location];
|
|
||||||
|
|
||||||
const membership = await Membership.findById(membershipId);
|
req.targetMembership = await validateClientForMembership({
|
||||||
|
authData: req.authData,
|
||||||
if (!membership) throw new Error('Failed to find target membership');
|
membershipId: new Types.ObjectId(membershipId),
|
||||||
|
|
||||||
const userMembership = await Membership.findOne({
|
|
||||||
workspace: membership.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userMembership) throw new Error('Failed to validate own membership')
|
|
||||||
|
|
||||||
const targetMembership = await validateMembership({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId: membership.workspace,
|
|
||||||
acceptedRoles
|
acceptedRoles
|
||||||
});
|
});
|
||||||
|
|
||||||
req.targetMembership = targetMembership;
|
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
|
||||||
return next(UnauthorizedRequestError({
|
|
||||||
message: 'Unable to validate workspace membership'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
MembershipOrg
|
MembershipOrg
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { validateMembershipOrg } from '../helpers/membershipOrg';
|
import {
|
||||||
|
validateClientForMembershipOrg,
|
||||||
|
validateMembershipOrg
|
||||||
|
} from '../helpers/membershipOrg';
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: transform
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,32 +24,23 @@ type req = 'params' | 'body' | 'query';
|
|||||||
const requireMembershipOrgAuth = ({
|
const requireMembershipOrgAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
acceptedStatuses,
|
acceptedStatuses,
|
||||||
location = 'params'
|
locationMembershipOrgId = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
|
||||||
acceptedStatuses: Array<'invited' | 'accepted'>;
|
acceptedStatuses: Array<'invited' | 'accepted'>;
|
||||||
location?: req;
|
locationMembershipOrgId?: req;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
const { membershipId } = req[locationMembershipOrgId];
|
||||||
const { membershipId } = req[location];
|
|
||||||
const membershipOrg = await MembershipOrg.findById(membershipId);
|
req.membershipOrg = await validateClientForMembershipOrg({
|
||||||
|
authData: req.authData,
|
||||||
if (!membershipOrg) throw new Error('Failed to find target organization membership');
|
membershipOrgId: new Types.ObjectId(membershipId),
|
||||||
|
acceptedRoles,
|
||||||
req.targetMembership = await validateMembershipOrg({
|
acceptedStatuses
|
||||||
userId: req.user._id,
|
});
|
||||||
organizationId: membershipOrg.organization,
|
|
||||||
acceptedRoles,
|
return next();
|
||||||
acceptedStatuses
|
|
||||||
});
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
return next(UnauthorizedRequestError({
|
|
||||||
message: 'Unable to validate organization membership'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ const requireOrganizationAuth = ({
|
|||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const { organizationId } = req[locationOrganizationId];
|
const { organizationId } = req[locationOrganizationId];
|
||||||
|
|
||||||
// TODO: incorporate [acceptedRoles] and [acceptedStatuses]
|
|
||||||
|
|
||||||
const { organization, membershipOrg } = await validateClientForOrganization({
|
const { organization, membershipOrg } = await validateClientForOrganization({
|
||||||
authData: req.authData,
|
authData: req.authData,
|
||||||
organizationId: new Types.ObjectId(organizationId),
|
organizationId: new Types.ObjectId(organizationId),
|
||||||
|
|||||||
@@ -17,32 +17,28 @@ import {
|
|||||||
const requireSecretAuth = ({
|
const requireSecretAuth = ({
|
||||||
acceptedRoles
|
acceptedRoles
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
const { secretId } = req.params;
|
||||||
const { secretId } = req.params;
|
|
||||||
|
const secret = await Secret.findById(secretId);
|
||||||
const secret = await Secret.findById(secretId);
|
|
||||||
|
if (!secret) {
|
||||||
if (!secret) {
|
return next(SecretNotFoundError({
|
||||||
return next(SecretNotFoundError({
|
message: 'Failed to find secret'
|
||||||
message: 'Failed to find secret'
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
await validateMembership({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId: secret.workspace,
|
|
||||||
acceptedRoles
|
|
||||||
});
|
|
||||||
|
|
||||||
req._secret = secret;
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (err) {
|
|
||||||
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id,
|
||||||
|
workspaceId: secret.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
req._secret = secret;
|
||||||
|
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const requireServiceTokenDataAuth = ({
|
|||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
location = 'params'
|
location = 'params'
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
location?: req;
|
location?: req;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const requireWorkspaceAuth = ({
|
|||||||
locationEnvironment = undefined,
|
locationEnvironment = undefined,
|
||||||
requiredPermissions = []
|
requiredPermissions = []
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
locationWorkspaceId: req;
|
locationWorkspaceId: req;
|
||||||
locationEnvironment?: req | undefined;
|
locationEnvironment?: req | undefined;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
@@ -34,6 +34,7 @@ const requireWorkspaceAuth = ({
|
|||||||
authData: req.authData,
|
authData: req.authData,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
|
acceptedRoles,
|
||||||
requiredPermissions
|
requiredPermissions
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ router.patch( // TODO - rewire dashboard to this route
|
|||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: 'params'
|
||||||
}),
|
}),
|
||||||
requireMembershipAuth({
|
requireMembershipAuth({
|
||||||
acceptedRoles: [ADMIN]
|
acceptedRoles: [ADMIN],
|
||||||
|
locationMembershipId: 'params'
|
||||||
}),
|
}),
|
||||||
workspaceController.updateWorkspaceMembership
|
workspaceController.updateWorkspaceMembership
|
||||||
);
|
);
|
||||||
@@ -124,7 +125,8 @@ router.delete( // TODO - rewire dashboard to this route
|
|||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: 'params'
|
||||||
}),
|
}),
|
||||||
requireMembershipAuth({
|
requireMembershipAuth({
|
||||||
acceptedRoles: [ADMIN]
|
acceptedRoles: [ADMIN],
|
||||||
|
locationMembershipId: 'params'
|
||||||
}),
|
}),
|
||||||
workspaceController.deleteWorkspaceMembership
|
workspaceController.deleteWorkspaceMembership
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -202,4 +202,13 @@ export const ServiceAccountKeyNotFoundError = (error?: Partial<RequestErrorConte
|
|||||||
stack: error?.stack
|
stack: error?.stack
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const BotNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
|
statusCode: error?.statusCode ?? 404,
|
||||||
|
type: error?.type ?? 'bot_not_found_error',
|
||||||
|
message: error?.message ?? 'The requested bot was not found',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
//* ----->[MISC ERRORS]<-----
|
//* ----->[MISC ERRORS]<-----
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Quickstart"
|
title: "Quickstart"
|
||||||
description: "Start managing your developer secrets and configs with Infisical in 10 minutes."
|
description: "Start managing developer secrets and configs with Infisical in minutes."
|
||||||
---
|
---
|
||||||
|
|
||||||
These examples demonstrate how to store and fetch environment variables from [Infisical Cloud](https://app.infisical.com) into your application.
|
These examples demonstrate how to store and fetch environment variables from [Infisical Cloud](https://app.infisical.com) into your application.
|
||||||
|
|||||||
@@ -42,7 +42,5 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi
|
|||||||
| [Flask](/integrations/frameworks/flask) | Framework | Available |
|
| [Flask](/integrations/frameworks/flask) | Framework | Available |
|
||||||
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
|
| [Laravel](/integrations/frameworks/laravel) | Framework | Available |
|
||||||
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
|
| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available |
|
||||||
| GCP | Cloud | Coming soon |
|
| GCP Secret Manager | Cloud | Coming soon |
|
||||||
| DigitalOcean | Cloud | Coming soon |
|
|
||||||
| GitHub Actions | CI/CD | Coming soon |
|
|
||||||
| Jenkins | CI/CD | Coming soon |
|
| Jenkins | CI/CD | Coming soon |
|
||||||
|
|||||||
Reference in New Issue
Block a user