diff --git a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts index f43e9c315..af4d1e21c 100644 --- a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts +++ b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts @@ -15,32 +15,28 @@ import { const requireSecretSnapshotAuth = ({ acceptedRoles, }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { secretSnapshotId } = req.params; - - const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId); - - if (!secretSnapshot) { - return next(SecretSnapshotNotFoundError({ - 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' })); + const { secretSnapshotId } = req.params; + + const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId); + + if (!secretSnapshot) { + return next(SecretSnapshotNotFoundError({ + message: 'Failed to find secret snapshot' + })); } + + await validateMembership({ + userId: req.user._id, + workspaceId: secretSnapshot.workspace, + acceptedRoles + }); + + req.secretSnapshot = secretSnapshot as any; + + next(); } } diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 8e93919a0..3e956a388 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -7,7 +7,12 @@ import { } from '../../../middleware'; import { query, param, body } from 'express-validator'; import { secretController } from '../../controllers/v1'; -import { ADMIN, MEMBER } from '../../../variables'; +import { + ADMIN, + MEMBER, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from '../../../variables'; router.get( '/:secretId/secret-versions', @@ -15,7 +20,8 @@ router.get( acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS] }), param('secretId').exists().trim(), query('offset').exists().isInt(), @@ -30,7 +36,8 @@ router.post( acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] }), param('secretId').exists().trim(), body('version').exists().isInt(), diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 5cfbeebf5..1e242778d 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -1,10 +1,16 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Bot, BotKey, Secret, ISecret, - IUser + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData } from '../models'; import { generateKeyPair, @@ -12,8 +18,88 @@ import { decryptSymmetric, decryptAsymmetric } 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 { 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] @@ -222,6 +308,7 @@ const decryptSymmetricHelper = async ({ } export { + validateClientForBot, createBot, getSecretsHelper, encryptSymmetricHelper, diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 27afbb4b1..fbbc2f807 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -1,17 +1,42 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Bot, Integration, IntegrationAuth, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData } from '../models'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { BotService } from '../services'; import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY, INTEGRATION_VERCEL, INTEGRATION_NETLIFY } from '../variables'; -import { UnauthorizedRequestError } from '../utils/errors'; +import { + UnauthorizedRequestError, + IntegrationAuthNotFoundError, + IntegrationNotFoundError +} from '../utils/errors'; import RequestError from '../utils/requestError'; +import { + validateClientForIntegrationAuth +} from '../helpers/integrationAuth'; +import { + validateUserClientForWorkspace +} from '../helpers/user'; +import { + validateServiceAccountClientForWorkspace +} from '../helpers/serviceAccount'; +import { IntegrationService } from '../services'; interface Update { workspace: string; @@ -20,6 +45,84 @@ interface Update { accountId?: string; } +/** + * Validate authenticated clients for integration with id [integrationId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.integrationId - id of integration 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 + */ + const validateClientForIntegration = async ({ + authData, + integrationId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + integrationId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + + const integration = await Integration.findById(integrationId); + if (!integration) throw IntegrationNotFoundError(); + + const integrationAuth = await IntegrationAuth + .findById(integration.integrationAuth) + .select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); + + if (!integrationAuth) throw IntegrationAuthNotFoundError(); + + const accessToken = (await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + })).accessToken; + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integration.workspace, + acceptedRoles + }); + + return ({ integration, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: integration.workspace + }); + + return ({ integration, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for integration' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integration.workspace, + acceptedRoles + }); + + return ({ integration, accessToken }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for integration' + }); +} + /** * Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration * named [integration] @@ -140,7 +243,7 @@ const syncIntegrationsHelper = async ({ // get integration auth access token const access = await getIntegrationAuthAccessHelper({ - integrationAuthId: integration.integrationAuth.toString() + integrationAuthId: integration.integrationAuth }); // sync secrets to integration @@ -167,7 +270,7 @@ const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ - const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { + const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { let refreshToken; try { @@ -204,7 +307,7 @@ const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @returns {String} accessToken - decrypted access token */ -const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { +const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { let accessId; let accessToken; try { @@ -367,6 +470,7 @@ const setIntegrationAuthAccessHelper = async ({ } export { + validateClientForIntegration, handleOAuthExchangeHelper, syncIntegrationsHelper, getIntegrationAuthRefreshHelper, diff --git a/backend/src/helpers/integrationAuth.ts b/backend/src/helpers/integrationAuth.ts index e69de29bb..c169fb799 100644 --- a/backend/src/helpers/integrationAuth.ts +++ b/backend/src/helpers/integrationAuth.ts @@ -0,0 +1,108 @@ +import { Types } from 'mongoose'; +import { + IntegrationAuth, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData, + IWorkspace +} from '../models'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; +import { + IntegrationAuthNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; +import { IntegrationService } from '../services'; +import { validateUserClientForWorkspace } from '../helpers/user'; +import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; + +/** + * Validate authenticated clients for integration authorization with id [integrationAuthId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.integrationAuthId - id of integration authorization to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ + const validateClientForIntegrationAuth = async ({ + authData, + integrationAuthId, + acceptedRoles, + attachAccessToken +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + integrationAuthId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; + attachAccessToken?: boolean; +}) => { + + const integrationAuth = await IntegrationAuth + .findById(integrationAuthId) + .populate<{ workspace: IWorkspace }>('workspace') + .select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); + + if (!integrationAuth) throw IntegrationAuthNotFoundError(); + + let accessToken; + if (attachAccessToken) { + accessToken = (await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + })).accessToken; + } + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integrationAuth.workspace._id, + acceptedRoles + }); + + return ({ integrationAuth, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: integrationAuth.workspace._id + }); + + return ({ integrationAuth, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for integration authorization' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integrationAuth.workspace._id, + acceptedRoles + }); + + return ({ integrationAuth, accessToken }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for integration authorization' + }); +} + +export { + validateClientForIntegrationAuth +}; \ No newline at end of file diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 93c1ac7e9..503ca9fc6 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -1,10 +1,106 @@ import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; -import { Membership, Key } from '../models'; +import { + Membership, + Key, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData +} from '../models'; import { MembershipNotFoundError, - BadRequestError + BadRequestError, + UnauthorizedRequestError } 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] @@ -21,7 +117,7 @@ const validateMembership = async ({ }: { userId: Types.ObjectId; workspaceId: Types.ObjectId; - acceptedRoles?: string[]; + acceptedRoles?: Array<'admin' | 'member'>; }) => { const membership = await Membership.findOne({ @@ -35,7 +131,7 @@ const validateMembership = async ({ if (acceptedRoles) { if (!acceptedRoles.includes(membership.role)) { - throw BadRequestError({ message: 'Failed to validate workspace membership role' }); + throw BadRequestError({ message: 'Failed authorization for membership role' }); } } @@ -134,6 +230,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { }; export { + validateClientForMembership, validateMembership, addMemberships, findMembership, diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index 65c1e01c2..b34e5dd2f 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -1,10 +1,98 @@ import * as Sentry from '@sentry/node'; 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 { MembershipOrgNotFoundError, - BadRequestError + BadRequestError, + UnauthorizedRequestError } 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] @@ -22,8 +110,8 @@ const validateMembershipOrg = async ({ }: { userId: Types.ObjectId; organizationId: Types.ObjectId; - acceptedRoles: string[]; - acceptedStatuses: string[]; + acceptedRoles?: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses?: Array<'invited' | 'accepted'>; }) => { const membershipOrg = await MembershipOrg.findOne({ user: userId, @@ -34,12 +122,16 @@ const validateMembershipOrg = async ({ throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' }); } - if (!acceptedRoles.includes(membershipOrg.role)) { - throw BadRequestError({ message: 'Failed to validate organization membership role' }); + if (acceptedRoles) { + if (!acceptedRoles.includes(membershipOrg.role)) { + throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' }); + } } - - if (!acceptedStatuses.includes(membershipOrg.status)) { - throw BadRequestError({ message: 'Failed to validate organization membership status' }); + + if (acceptedStatuses) { + if (!acceptedStatuses.includes(membershipOrg.status)) { + throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' }); + } } return membershipOrg; @@ -164,6 +256,7 @@ const deleteMembershipOrg = async ({ }; export { + validateClientForMembershipOrg, validateMembershipOrg, findMembershipOrg, addMembershipsOrg, diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 0784f446b..9840c9075 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -15,7 +15,8 @@ import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY + AUTH_MODE_API_KEY, + OWNER } from '../variables'; import { getStripeSecretKey, @@ -24,8 +25,15 @@ import { getStripeProductStarter } from '../config'; import { - UnauthorizedRequestError + UnauthorizedRequestError, + OrganizationNotFoundError } from '../utils/errors'; +import { + validateUserClientForOrganization +} from '../helpers/user'; +import { + validateServiceAccountClientForOrganization +} from '../helpers/serviceAccount'; /** * Validate accepted clients for organization with id [organizationId] @@ -35,34 +43,66 @@ import { */ const validateClientForOrganization = async ({ authData, - organizationId + organizationId, + acceptedRoles, + acceptedStatuses }: { authData: { authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }, - organizationId: string; + organizationId: Types.ObjectId; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; }) => { - // TODO + + const organization = await Organization.findById(organizationId); + + if (!organization) { + throw OrganizationNotFoundError({ + message: 'Failed to find organization' + }); + } if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - // TODO + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses + }); + + return ({ organization, membershipOrg }); } if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - // TODO + await validateServiceAccountClientForOrganization({ + serviceAccount: authData.authPayload, + organization + }); + + return ({ organization }); } if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - // TODO + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for organization' + }); } if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - // TODO + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses + }); + + return ({ organization, membershipOrg }); } throw UnauthorizedRequestError({ - message: 'Failed client authorization for organization resource' + message: 'Failed client authorization for organization' }); } @@ -228,6 +268,7 @@ const updateSubscriptionOrgQuantity = async ({ }; export { + validateClientForOrganization, createOrganization, initSubscriptionOrg, updateSubscriptionOrgQuantity diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 09418560d..7d3e54105 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -10,15 +10,24 @@ import { ISecret } from '../models'; import { + validateMembership +} from '../helpers/membership'; +import { + validateUserClientForSecret, validateUserClientForSecrets } from '../helpers/user'; import { - validateServiceTokenDataClientForSecrets + validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData'; import { - validateServiceAccountClientForSecrets + validateServiceAccountClientForSecrets, + validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { + BadRequestError, + UnauthorizedRequestError, + SecretNotFoundError +} from '../utils/errors'; import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, @@ -27,12 +36,91 @@ import { } from '../variables'; /** - * Validate accepted clients for secrets with ids [secretIds] + * Validate authenticated clients for secrets with id [secretId] based + * on any known permissions. * @param {Object} obj - * @param {User} obj.user - user client - * @param {ServiceAccount} obj.serviceAccount - service account client - * @param {ServiceTokenData} obj.service - service token client - * @param {String[]} obj.secretIds - ids of secrets to validate against + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.secretId - id of secret to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +const validateClientForSecret = async ({ + authData, + secretId, + acceptedRoles, + requiredPermissions +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, + secretId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions: string[]; +}) => { + const secret = await Secret.findById(secretId); + + if (!secret) throw SecretNotFoundError({ + message: 'Failed to find secret' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for secret' + }); +} + +/** + * Validate authenticated clients for secrets with ids [secretIds] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId[]} obj.secretIds - id of 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 */ const validateClientForSecrets = async ({ authData, @@ -43,7 +131,7 @@ const validateClientForSecrets = async ({ authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }, - secretIds: string[]; + secretIds: Types.ObjectId[]; requiredPermissions: string[]; }) => { @@ -51,7 +139,7 @@ const validateClientForSecrets = async ({ secrets = await Secret.find({ _id: { - $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) + $in: secretIds } }); @@ -105,5 +193,6 @@ const validateClientForSecrets = async ({ } export { + validateClientForSecret, validateClientForSecrets } \ No newline at end of file diff --git a/backend/src/helpers/serviceAccount.ts b/backend/src/helpers/serviceAccount.ts index f0fe3063e..892767259 100644 --- a/backend/src/helpers/serviceAccount.ts +++ b/backend/src/helpers/serviceAccount.ts @@ -8,6 +8,8 @@ import { ServiceTokenData, IServiceTokenData, ISecret, + IOrganization, + IServiceAccountWorkspacePermission, ServiceAccountWorkspacePermission } from '../models'; import { @@ -109,21 +111,20 @@ const validateClientForServiceAccount = async ({ environment?: string; requiredPermissions?: string[]; }) => { - // TODO: add service account API support for workspace-level endpoints that are not - // tied to any specific 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({ serviceAccount, workspace: new Types.ObjectId(workspaceId), environment }); - + if (!permission) throw UnauthorizedRequestError({ message: 'Failed service account authorization for the given workspace environment' }); - - // TODO: refactor + let runningIsDisallowed = false; requiredPermissions?.forEach((requiredPermission: string) => { switch (requiredPermission) { @@ -143,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' + }); } } @@ -180,7 +195,6 @@ const validateClientForServiceAccount = async ({ }); requiredPermissions?.forEach((requiredPermission: string) => { - // TODO: refactor let runningIsDisallowed = false; requiredPermissions?.forEach((requiredPermission: string) => { switch (requiredPermission) { @@ -202,9 +216,6 @@ const validateClientForServiceAccount = async ({ }); }); }); - - // TODO - return []; } /** @@ -231,9 +242,30 @@ const validateServiceAccountClientForServiceAccount = ({ } } +/** + * Validate that service account (client) can access organization [organization] + * @param {Object} obj + * @param {User} obj.user - service account client + * @param {Organization} obj.organization - organization to validate against + */ +const validateServiceAccountClientForOrganization = async ({ + serviceAccount, + organization +}: { + serviceAccount: IServiceAccount; + organization: IOrganization; +}) => { + if (!serviceAccount.organization.equals(organization._id)) { + throw UnauthorizedRequestError({ + message: 'Failed service account authorization for the given organization' + }); + } +} + export { validateClientForServiceAccount, validateServiceAccountClientForWorkspace, validateServiceAccountClientForSecrets, - validateServiceAccountClientForServiceAccount + validateServiceAccountClientForServiceAccount, + validateServiceAccountClientForOrganization } \ No newline at end of file diff --git a/backend/src/helpers/serviceTokenData.ts b/backend/src/helpers/serviceTokenData.ts index 70c9d416b..9a8bb1288 100644 --- a/backend/src/helpers/serviceTokenData.ts +++ b/backend/src/helpers/serviceTokenData.ts @@ -1,9 +1,94 @@ import { Types } from 'mongoose'; import { ISecret, - IServiceTokenData + IServiceTokenData, + ServiceTokenData, + IUser, + User, + IServiceAccount, + ServiceAccount, } from '../models'; -import { UnauthorizedRequestError } from '../utils/errors'; +import { + UnauthorizedRequestError, + ServiceTokenDataNotFoundError +} 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'; + +/** + * Validate authenticated clients for service token with id [serviceTokenId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + */ +const validateClientForServiceTokenData = async ({ + authData, + serviceTokenDataId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + serviceTokenDataId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + const serviceTokenData = await ServiceTokenData + .findById(serviceTokenDataId) + .select('+encryptedKey +iv +tag') + .populate<{ user: IUser }>('user'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ + message: 'Failed to find service token data' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: serviceTokenData.workspace, + acceptedRoles + }); + + return serviceTokenData; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: serviceTokenData.workspace + }); + + return serviceTokenData; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for service token data' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: serviceTokenData.workspace, + acceptedRoles + }); + + return serviceTokenData; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for service token data' + }); +} /** * Validate that service token (client) can access workspace @@ -34,20 +119,24 @@ import { UnauthorizedRequestError } from '../utils/errors'; }); } - if (serviceTokenData.environment !== environment) { - // case: invalid environment passed - throw UnauthorizedRequestError({ - message: 'Failed service token authorization for the given workspace environment' - }); - } - - requiredPermissions?.forEach((permission) => { - if (!serviceTokenData.permissions.includes(permission)) { + if (environment) { + // case: environment is specified + + if (serviceTokenData.environment !== environment) { + // case: invalid environment passed 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}` + }); + } + }); + } } /** @@ -94,6 +183,7 @@ import { UnauthorizedRequestError } from '../utils/errors'; } export { + validateClientForServiceTokenData, validateServiceTokenDataClientForWorkspace, validateServiceTokenDataClientForSecrets } \ No newline at end of file diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index ddf313da9..5a33f3d2e 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -5,7 +5,9 @@ import { ISecret, IServiceAccount, User, - Membership + Membership, + IOrganization, + Organization, } from '../models'; import { sendMail } from './nodemailer'; import { validateMembership } from './membership'; @@ -177,21 +179,23 @@ const validateUserClientForWorkspace = async ({ user, workspaceId, environment, + acceptedRoles, requiredPermissions }: { user: IUser; workspaceId: Types.ObjectId; environment?: string; + acceptedRoles: Array<'admin' | 'member'>; requiredPermissions?: string[]; }) => { // validate user membership in workspace const membership = await validateMembership({ userId: user._id, - workspaceId + workspaceId, + acceptedRoles }); - // TODO: refactor let runningIsDisallowed = false; requiredPermissions?.forEach((requiredPermission: string) => { switch (requiredPermission) { @@ -215,6 +219,42 @@ const validateUserClientForWorkspace = async ({ return membership; } +/** + * Validate that user (client) can access secret [secret] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Secret[]} obj.secrets - secrets to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateUserClientForSecret = async ({ + user, + secret, + acceptedRoles, + requiredPermissions +}: { + user: IUser; + secret: ISecret; + acceptedRoles?: Array<'admin' | 'member'>; + requiredPermissions?: string[]; +}) => { + const membership = await validateMembership({ + userId: user._id, + workspaceId: secret.workspace, + acceptedRoles + }); + + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: 'You do not have the required permissions to perform this action' + }); + } + } +} + /** * Validate that user (client) can access secrets [secrets] * with required permissions [requiredPermissions] @@ -232,7 +272,8 @@ const validateUserClientForWorkspace = async ({ secrets: ISecret[]; requiredPermissions?: string[]; }) => { - // TODO: refactor + + // TODO: add acceptedRoles? const userMemberships = await Membership.find({ user: user._id }) const userMembershipById = _.keyBy(userMemberships, 'workspace'); @@ -288,11 +329,40 @@ const validateUserClientForServiceAccount = async ({ } } +/** + * Validate that user (client) can access organization [organization] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Organization} obj.organization - organization to validate against + */ + const validateUserClientForOrganization = async ({ + user, + organization, + acceptedRoles, + acceptedStatuses +}: { + user: IUser; + organization: IOrganization; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; +}) => { + const membershipOrg = await validateMembershipOrg({ + userId: user._id, + organizationId: organization._id, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; +} + export { setupAccount, completeAccount, checkUserDevice, validateUserClientForWorkspace, validateUserClientForSecrets, - validateUserClientForServiceAccount + validateUserClientForServiceAccount, + validateUserClientForOrganization, + validateUserClientForSecret }; diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 7b077e7d1..f3b27bf10 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -19,7 +19,7 @@ import { validateUserClientForWorkspace } from '../helpers/user'; import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData'; import { validateMembership } from '../helpers/membership'; -import { UnauthorizedRequestError } from '../utils/errors'; +import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors'; import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, @@ -34,28 +34,38 @@ import { * @param {Object} obj.authData - authenticated client details * @param {Types.ObjectId} obj.workspaceId - id of 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 */ const validateClientForWorkspace = async ({ authData, workspaceId, environment, + acceptedRoles, requiredPermissions }: { authData: { authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; - }, + }; workspaceId: Types.ObjectId; environment?: string; + acceptedRoles: Array<'admin' | 'member'>; 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) { const membership = await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId, environment, + acceptedRoles, requiredPermissions }); @@ -89,6 +99,7 @@ const validateClientForWorkspace = async ({ user: authData.authPayload, workspaceId, environment, + acceptedRoles, requiredPermissions }); @@ -96,7 +107,7 @@ const validateClientForWorkspace = async ({ } throw UnauthorizedRequestError({ - message: 'Failed client authorization for workspace resource' + message: 'Failed client authorization for workspace' }); } diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index fd828c89b..16191d607 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -44,6 +44,7 @@ const requireAuth = ({ acceptedAuthModes: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { + // validate auth token against accepted auth modes [acceptedAuthModes] // and return token type [authTokenType] and value [authTokenValue] const { authMode, authTokenValue } = validateAuthMode({ @@ -87,7 +88,7 @@ const requireAuth = ({ req.authData = { authMode, - authPayload + authPayload // User, ServiceAccount, ServiceTokenData } return next(); diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts index c06f1c861..089f570c8 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -1,32 +1,28 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { Bot } from '../models'; import { validateMembership } from '../helpers/membership'; +import { validateClientForBot } from '../helpers/bot'; import { AccountNotFoundError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; const requireBotAuth = ({ acceptedRoles, - location = 'params' + locationBotId = 'params' }: { - acceptedRoles: string[]; - location?: req; + acceptedRoles: Array<'admin' | 'member'>; + locationBotId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - const bot = await Bot.findById(req[location].botId); + const { botId } = req[locationBotId]; - if (!bot) { - return next(AccountNotFoundError({message: 'Failed to locate Bot account'})) - } - - await validateMembership({ - userId: req.user._id, - workspaceId: bot.workspace, + req.bot = await validateClientForBot({ + authData: req.authData, + botId: new Types.ObjectId(botId), acceptedRoles }); - req.bot = bot; - next(); } } diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index 51051584c..bcde94f34 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -1,7 +1,9 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { Integration, IntegrationAuth } from '../models'; import { IntegrationService } from '../services'; import { validateMembership } from '../helpers/membership'; +import { validateClientForIntegration } from '../helpers/integration'; import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors'; /** @@ -13,42 +15,24 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err const requireIntegrationAuth = ({ acceptedRoles }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; }) => { return async (req: Request, res: Response, next: NextFunction) => { - // integration authorization middleware - const { integrationId } = req.params; - // validate integration accessibility - const integration = await Integration.findOne({ - _id: integrationId - }); - - if (!integration) { - return next(IntegrationNotFoundError({message: 'Failed to locate Integration'})) - } - - await validateMembership({ - userId: req.user._id, - workspaceId: integration.workspace, + const { integration, accessToken } = await validateClientForIntegration({ + authData: req.authData, + integrationId: new Types.ObjectId(integrationId), acceptedRoles }); - const integrationAuth = await IntegrationAuth.findOne({ - _id: integration.integrationAuth - }).select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); - - if (!integrationAuth) { - return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'})) + if (integration) { + req.integration = integration; + } + + if (accessToken) { + req.accessToken = accessToken; } - - req.integration = integration; - req.accessToken = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); return next(); }; diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index 07f347ecc..af1eb1a53 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -1,7 +1,9 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; import { IntegrationAuth, IWorkspace } from '../models'; import { IntegrationService } from '../services'; +import { validateClientForIntegrationAuth } from '../helpers/integrationAuth'; import { validateMembership } from '../helpers/membership'; import { UnauthorizedRequestError } from '../utils/errors'; @@ -19,36 +21,26 @@ const requireIntegrationAuthorizationAuth = ({ attachAccessToken = true, location = 'params' }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; attachAccessToken?: boolean; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { integrationAuthId } = req[location]; - const integrationAuth = await IntegrationAuth.findOne({ - _id: integrationAuthId - }) - .populate<{ workspace: IWorkspace }>('workspace') - .select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); - if (!integrationAuth) { - return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'})) - } - - await validateMembership({ - userId: req.user._id, - workspaceId: integrationAuth.workspace._id, - acceptedRoles + const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({ + authData: req.authData, + integrationAuthId: new Types.ObjectId(integrationAuthId), + acceptedRoles, + attachAccessToken }); + + if (integrationAuth) { + req.integrationAuth = integrationAuth; + } - req.integrationAuth = integrationAuth; - if (attachAccessToken) { - const access = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); - req.accessToken = access.accessToken; + if (accessToken) { + req.accessToken = accessToken; } return next(); diff --git a/backend/src/middleware/requireMembershipAuth.ts b/backend/src/middleware/requireMembershipAuth.ts index 136fabadb..851230371 100644 --- a/backend/src/middleware/requireMembershipAuth.ts +++ b/backend/src/middleware/requireMembershipAuth.ts @@ -1,9 +1,13 @@ +import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; import { UnauthorizedRequestError } from '../utils/errors'; import { Membership, } from '../models'; -import { validateMembership } from '../helpers/membership'; +import { + validateClientForMembership, + validateMembership +} from '../helpers/membership'; type req = 'params' | 'body' | 'query'; @@ -16,43 +20,25 @@ type req = 'params' | 'body' | 'query'; */ const requireMembershipAuth = ({ acceptedRoles, - location = 'params' + locationMembershipId = 'params' }: { - acceptedRoles: string[]; - location?: req; + acceptedRoles: Array<'admin' | 'member'>; + locationMembershipId: req }) => { return async ( req: Request, res: Response, next: NextFunction ) => { - try { - const { membershipId } = req[location]; - - const membership = await Membership.findById(membershipId); - - if (!membership) throw new Error('Failed to find target membership'); - - 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 - }); - - req.targetMembership = targetMembership; - - return next(); - } catch (err) { - return next(UnauthorizedRequestError({ - message: 'Unable to validate workspace membership' - })); - } + const { membershipId } = req[locationMembershipId]; + + req.targetMembership = await validateClientForMembership({ + authData: req.authData, + membershipId: new Types.ObjectId(membershipId), + acceptedRoles + }); + + return next(); } } diff --git a/backend/src/middleware/requireMembershipOrgAuth.ts b/backend/src/middleware/requireMembershipOrgAuth.ts index ea9ed9afc..b34c9c5e2 100644 --- a/backend/src/middleware/requireMembershipOrgAuth.ts +++ b/backend/src/middleware/requireMembershipOrgAuth.ts @@ -1,11 +1,17 @@ +import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; import { UnauthorizedRequestError } from '../utils/errors'; import { MembershipOrg } from '../models'; -import { validateMembershipOrg } from '../helpers/membershipOrg'; +import { + validateClientForMembershipOrg, + validateMembershipOrg +} from '../helpers/membershipOrg'; +// TODO: transform + type req = 'params' | 'body' | 'query'; /** @@ -18,32 +24,23 @@ type req = 'params' | 'body' | 'query'; const requireMembershipOrgAuth = ({ acceptedRoles, acceptedStatuses, - location = 'params' + locationMembershipOrgId = 'params' }: { - acceptedRoles: string[]; - acceptedStatuses: string[]; - location?: req; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; + locationMembershipOrgId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { membershipId } = req[location]; - const membershipOrg = await MembershipOrg.findById(membershipId); - - if (!membershipOrg) throw new Error('Failed to find target organization membership'); - - req.targetMembership = await validateMembershipOrg({ - userId: req.user._id, - organizationId: membershipOrg.organization, - acceptedRoles, - acceptedStatuses - }); - - return next(); - } catch (err) { - return next(UnauthorizedRequestError({ - message: 'Unable to validate organization membership' - })); - } + const { membershipId } = req[locationMembershipOrgId]; + + req.membershipOrg = await validateClientForMembershipOrg({ + authData: req.authData, + membershipOrgId: new Types.ObjectId(membershipId), + acceptedRoles, + acceptedStatuses + }); + + return next(); } } diff --git a/backend/src/middleware/requireOrganizationAuth.ts b/backend/src/middleware/requireOrganizationAuth.ts index 5768b4b36..f6d8eb8ce 100644 --- a/backend/src/middleware/requireOrganizationAuth.ts +++ b/backend/src/middleware/requireOrganizationAuth.ts @@ -3,6 +3,7 @@ import { Types } from 'mongoose'; import { IOrganization, MembershipOrg } from '../models'; import { UnauthorizedRequestError, ValidationError } from '../utils/errors'; import { validateMembershipOrg } from '../helpers/membershipOrg'; +import { validateClientForOrganization } from '../helpers/organization'; type req = 'params' | 'body' | 'query'; @@ -16,20 +17,29 @@ type req = 'params' | 'body' | 'query'; const requireOrganizationAuth = ({ acceptedRoles, acceptedStatuses, - location = 'params' + locationOrganizationId = 'params' }: { - acceptedRoles: string[]; - acceptedStatuses: string[]; - location?: req; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; + locationOrganizationId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - const { organizationId } = req[location]; - req.membershipOrg = await validateMembershipOrg({ - userId: req.user._id, + const { organizationId } = req[locationOrganizationId]; + + const { organization, membershipOrg } = await validateClientForOrganization({ + authData: req.authData, organizationId: new Types.ObjectId(organizationId), acceptedRoles, acceptedStatuses }); + + if (organization) { + req.organization = organization; + } + + if (membershipOrg) { + req.membershipOrg = membershipOrg; + } return next(); }; diff --git a/backend/src/middleware/requireSecretAuth.ts b/backend/src/middleware/requireSecretAuth.ts index c86b7b680..1462d67b0 100644 --- a/backend/src/middleware/requireSecretAuth.ts +++ b/backend/src/middleware/requireSecretAuth.ts @@ -1,12 +1,17 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; import { Secret } from '../models'; import { validateMembership } from '../helpers/membership'; +import { + validateClientForSecret +} from '../helpers/secrets'; // note: used for old /v1/secret and /v2/secret routes. -// newer /v2/secrets routes use [requireSecretsAuth] middleware +// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception +// of some /ee endpoints /** * Validate if user on request has proper membership to modify secret. @@ -15,34 +20,25 @@ import { * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireSecretAuth = ({ - acceptedRoles + acceptedRoles, + requiredPermissions }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { secretId } = req.params; - - const secret = await Secret.findById(secretId); - - if (!secret) { - return next(SecretNotFoundError({ - message: 'Failed to find secret' - })); - } - - await validateMembership({ - userId: req.user._id, - workspaceId: secret.workspace, - acceptedRoles - }); - - req._secret = secret; + const { secretId } = req.params; + + const secret = await validateClientForSecret({ + authData: req.authData, + secretId: new Types.ObjectId(secretId), + acceptedRoles, + requiredPermissions + }); + + req._secret = secret; - next(); - } catch (err) { - return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' })); - } + next(); } } diff --git a/backend/src/middleware/requireSecretsAuth.ts b/backend/src/middleware/requireSecretsAuth.ts index 5a550156a..a076a3f1a 100644 --- a/backend/src/middleware/requireSecretsAuth.ts +++ b/backend/src/middleware/requireSecretsAuth.ts @@ -1,4 +1,5 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { UnauthorizedRequestError } from '../utils/errors'; import { Secret, Membership } from '../models'; import { validateClientForSecrets } from '../helpers/secrets'; @@ -24,7 +25,7 @@ const requireSecretsAuth = ({ req.secrets = await validateClientForSecrets({ authData: req.authData, - secretIds: [req.body.secretIds], + secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), requiredPermissions }); diff --git a/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts b/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts index d28c72352..0ceb4f598 100644 --- a/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts +++ b/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts @@ -14,8 +14,8 @@ const requireServiceAccountWorkspacePermissionAuth = ({ acceptedStatuses, location = 'params' }: { - acceptedRoles: string[]; - acceptedStatuses: string[]; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { diff --git a/backend/src/middleware/requireServiceTokenDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts index 513cbe604..7715991ba 100644 --- a/backend/src/middleware/requireServiceTokenDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,5 +1,7 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { ServiceToken, ServiceTokenData } from '../models'; +import { validateClientForServiceTokenData } from '../helpers/serviceTokenData'; import { validateMembership } from '../helpers/membership'; import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors'; @@ -9,30 +11,17 @@ const requireServiceTokenDataAuth = ({ acceptedRoles, location = 'params' }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { serviceTokenDataId } = req[location]; - - const serviceTokenData = await ServiceTokenData - .findById(req[location].serviceTokenDataId) - .select('+encryptedKey +iv +tag').populate('user'); - - if (!serviceTokenData) { - return next(AccountNotFoundError({ message: 'Failed to locate service token data' })); - } - - if (req.user) { - // case: jwt auth - await validateMembership({ - userId: req.user._id, - workspaceId: serviceTokenData.workspace, - acceptedRoles - }); - } - - req.serviceTokenData = serviceTokenData; + + req.serviceTokenData = await validateClientForServiceTokenData({ + authData: req.authData, + serviceTokenDataId: new Types.ObjectId(serviceTokenDataId), + acceptedRoles + }); next(); } diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 64c1c37f2..5d094f972 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -19,13 +19,12 @@ const requireWorkspaceAuth = ({ locationEnvironment = undefined, requiredPermissions = [] }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; locationWorkspaceId: req; locationEnvironment?: req | undefined; requiredPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - const workspaceId = req[locationWorkspaceId]?.workspaceId; const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; @@ -34,6 +33,7 @@ const requireWorkspaceAuth = ({ authData: req.authData, workspaceId: new Types.ObjectId(workspaceId), environment, + acceptedRoles, requiredPermissions }); diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index e6014c675..ead969fc7 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from "mongoose"; +import { Schema, model, Types, Document } from "mongoose"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -16,7 +16,7 @@ import { INTEGRATION_SUPABASE, } from "../variables"; -export interface IIntegrationAuth { +export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager'; diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index b5013acbb..540a4451b 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -1,7 +1,7 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types, Document } from 'mongoose'; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables'; -export interface IMembershipOrg { +export interface IMembershipOrg extends Document { _id: Types.ObjectId; user: Types.ObjectId; inviteEmail: string; diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index ff9764ced..f2c825ba8 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -10,7 +10,9 @@ import { ADMIN, MEMBER, AUTH_MODE_JWT, - AUTH_MODE_SERVICE_TOKEN + AUTH_MODE_SERVICE_TOKEN, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS } from '../../variables'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret'; import { secretController } from '../../controllers/v2'; @@ -75,7 +77,8 @@ router.get( acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS] }), validateRequest, secretController.getSecret @@ -103,7 +106,8 @@ router.delete( acceptedAuthModes: [AUTH_MODE_JWT] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] }), param('secretId').isMongoId(), validateRequest, diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 792491557..57657b079 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -1,5 +1,6 @@ import express from 'express'; const router = express.Router(); +import { Types } from 'mongoose'; import { requireAuth, requireWorkspaceAuth, @@ -47,7 +48,7 @@ router.post( if (secretIds.length > 0) { req.secrets = await validateClientForSecrets({ authData: req.authData, - secretIds, + secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), requiredPermissions: [] }); } diff --git a/backend/src/routes/v2/serviceAccounts.ts b/backend/src/routes/v2/serviceAccounts.ts index fef0c87e9..6f0db91b7 100644 --- a/backend/src/routes/v2/serviceAccounts.ts +++ b/backend/src/routes/v2/serviceAccounts.ts @@ -53,7 +53,7 @@ router.post( requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED], - location: 'body' + locationOrganizationId: 'body' }), serviceAccountsController.createServiceAccount ); diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index d2180624b..e258636fb 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -106,7 +106,8 @@ router.patch( // TODO - rewire dashboard to this route locationWorkspaceId: 'params' }), requireMembershipAuth({ - acceptedRoles: [ADMIN] + acceptedRoles: [ADMIN], + locationMembershipId: 'params' }), workspaceController.updateWorkspaceMembership ); @@ -124,7 +125,8 @@ router.delete( // TODO - rewire dashboard to this route locationWorkspaceId: 'params' }), requireMembershipAuth({ - acceptedRoles: [ADMIN] + acceptedRoles: [ADMIN], + locationMembershipId: 'params' }), workspaceController.deleteWorkspaceMembership ); diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts index 7b3a20a6b..0fd634c33 100644 --- a/backend/src/services/IntegrationService.ts +++ b/backend/src/services/IntegrationService.ts @@ -1,3 +1,4 @@ +import { Types } from 'mongoose'; import { handleOAuthExchangeHelper, syncIntegrationsHelper, @@ -67,7 +68,7 @@ class IntegrationService { * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ - static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) { + static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: Types.ObjectId}) { return await getIntegrationAuthRefreshHelper({ integrationAuthId }); @@ -80,7 +81,7 @@ class IntegrationService { * @param {String} obj.integrationAuthId - id of integration auth * @param {String} accessToken - decrypted access token */ - static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) { + static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) { return await getIntegrationAuthAccessHelper({ integrationAuthId }); diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 4799728b9..987215091 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -73,6 +73,16 @@ export const ValidationError = (error?: Partial) => new Req stack: error?.stack }); +//* ----->[INTEGRATION AUTH ERRORS]<----- +export const IntegrationAuthNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'integration_auth_not_found_error', + message: error?.message ?? 'The requested integration authorization was not found', + context: error?.context, + stack: error?.stack +}); + //* ----->[INTEGRATION ERRORS]<----- export const IntegrationNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, @@ -202,4 +212,13 @@ export const ServiceAccountKeyNotFoundError = (error?: Partial) => 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]<----- diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index 7df8f10e5..3e18f8ce4 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -1,25 +1,51 @@ --- title: "Authentication" +description: "How to authenticate with the Infisical Public API" --- -To authenticate requests with Infisical, you can either use an API Key or [Infisical Token](../../../getting-started/dashboard/token); certain endpoints will accept either one or both. -- API Key: This general-purpose authentication token provides user access to most endpoints in this reference. -- [Infisical Token](../../../getting-started/dashboard/token): This authentication token (also referred to as the service token) is scoped to a specific project and environment and used for CRUD secret operations. +## Essentials + +The Public API accepts multiple modes of authentication being via API Key, Service Account credentials, or [Infisical Token](../../../getting-started/dashboard/token). + +- API Key: Provides full access to all endpoints representing the user. +- [Service Account](): Provides scoped access to an organization and select projects representing a machine such as a VM or application client. +- [Infisical Token](../../../getting-started/dashboard/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment. +The API key mode uses an API key to authenticate with the API. + To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key in User Settings > API Keys ![API key dashboard](../../images/api-key-dashboard.png) ![API key in personal settings](../../images/api-key-settings.png) + + +The Service Account mode uses an Access Key to authenticate with the API and a Public Key and Private Key to perform any cryptographic operations. + +To authenticate requests with Infisical using the Access Key, you must include it in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `. + +You can create a Service Account in Organization Settings > Service Accounts + -To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer st.`. + +The Infisical Token mode uses an Infisical Token to authenticate with the API. + +To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `. You can obtain an Infisical Token in Project Settings > Service Tokens. ![token add](../../images/project-token-add.png) - \ No newline at end of file + + +## Use Cases + +Depending on your use case, it may make sense to use one or another authentication mode: + +- API Key (not recommended): Use if you need full access to the Public API without needing to access any secrets endpoints (because API keys can't encrypt/decrypt secrets). +- Service Account (recommeded): Use if you need access to multiple projects and environments in an organization; service accounts can generate short-lived access tokens, making them useful for some complex setups. +- Service Token (recommeded): Use if you need short-lived, scoped CRUD access to the secrets of a specific project and environment. \ No newline at end of file diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 3d748314b..585abc8f0 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -2,11 +2,17 @@ title: "Introduction" --- -Infisical's REST API provides users an alternative way to programmatically access and manage +Infisical's Public (REST) API provides users an alternative way to programmatically access and manage secrets via HTTPS requests. This can be useful for automating tasks, such as rotating credentials, or for integrating secret management into a larger system. -With the REST API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more. +With the Public API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more. + + + We highly recommend using one of the available SDKs when working with the Infisical API. + + If you decide to make your own requests using the API reference instead, be prepared for a steeper learning curve and more manual work. + ## Concepts diff --git a/docs/getting-started/dashboard/organization.mdx b/docs/getting-started/dashboard/organization.mdx index 63ec2cd49..161a79f14 100644 --- a/docs/getting-started/dashboard/organization.mdx +++ b/docs/getting-started/dashboard/organization.mdx @@ -24,6 +24,14 @@ To add a member to your organization, scroll down to the "Organization Members" projects by default. +## Service Accounts + +Service accounts represent machine identities such as VMs or application clients that can authenticate with Infisical. They can be provisioned read/write permissions for project(s) and environment(s). + +To add a service account to your organization, scroll down to the "Service Accounts" section and create a service account. Afterwards, you can press on the edit button beside the service account to provision it permissions. + +![organization service accounts](../../images/organization-service-accounts.png) + ## Incident contacts Incident contacts of an organization are alerted if anything abnormal is detected within the operations of an organization. diff --git a/docs/getting-started/dashboard/project.mdx b/docs/getting-started/dashboard/project.mdx index ce7df57e3..065835fb3 100644 --- a/docs/getting-started/dashboard/project.mdx +++ b/docs/getting-started/dashboard/project.mdx @@ -25,16 +25,16 @@ In most cases, environment variables belong to specific environments: developmen ![project environment](../../images/project-environment.png) -### Personal/Shared scoping +### Personal overrides -Every environment variable is classified as either personal or shared. +Every environment variable value can be overriden with a custom value. -- A personal environment variable is one created by a user of a project to be available for that user only. -- A shared environment variable is one created by a user of a project to be available for other users of the project. +- An overriden value can only be read and accesssed by the user that overrode the original shared value. +- A (default) shared value can be read and accesssed by other users in a project. -You can toggle the classification of an environment variable by pressing on its settings: +You can turn overrides on/off by toggling the override/branch icon: -![project variable toggle open](../../images/project-envar-toggle-open.png) +![project variable toggle open](../../images/project-envar-override.png) ### Search @@ -42,12 +42,6 @@ You can search for any environment variable by its key. ![project search](../../images/project-search.png) -### Sort - -You can sort environment variables alphabetically by their keys. - -![project sort](../../images/project-sort.png) - ### Hide/Un-hide You can hide or un-hide the values of your environment variables. By default, the values are hidden for your privacy. diff --git a/docs/getting-started/introduction.mdx b/docs/getting-started/introduction.mdx index cd5022083..d70d12e1b 100644 --- a/docs/getting-started/introduction.mdx +++ b/docs/getting-started/introduction.mdx @@ -3,7 +3,7 @@ title: "Introduction" description: "What is Infisical?" --- -Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret manager that enables teams to easily manage and sync their environment variables. +Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret management platform that enables teams to easily manage and sync their environment variables. Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 256ae8f85..f19bd1120 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -1,6 +1,6 @@ --- 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. @@ -9,7 +9,7 @@ These examples demonstrate how to store and fetch environment variables from [In 1. Login or create an account at `app.infisical.com`. 2. Create a new project. -3. Populate your environment variables as in the image below. +3. Keep the default environment variables or populate them as in the image below. ![project quickstart](../images/project-quickstart.png) diff --git a/docs/images/dashboard-name-modal-organization.png b/docs/images/dashboard-name-modal-organization.png index 7fe84675e..59145d21b 100644 Binary files a/docs/images/dashboard-name-modal-organization.png and b/docs/images/dashboard-name-modal-organization.png differ diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png index 961b53587..2188debfc 100644 Binary files a/docs/images/dashboard.png and b/docs/images/dashboard.png differ diff --git a/docs/images/integrations-supabase-authorization.png b/docs/images/integrations-supabase-authorization.png new file mode 100644 index 000000000..d6daab30c Binary files /dev/null and b/docs/images/integrations-supabase-authorization.png differ diff --git a/docs/images/integrations-supabase-create.png b/docs/images/integrations-supabase-create.png new file mode 100644 index 000000000..b6c2cd35d Binary files /dev/null and b/docs/images/integrations-supabase-create.png differ diff --git a/docs/images/integrations-supabase-dashboard.png b/docs/images/integrations-supabase-dashboard.png new file mode 100644 index 000000000..4908cfc5a Binary files /dev/null and b/docs/images/integrations-supabase-dashboard.png differ diff --git a/docs/images/integrations-supabase-token.png b/docs/images/integrations-supabase-token.png new file mode 100644 index 000000000..00a3c0b20 Binary files /dev/null and b/docs/images/integrations-supabase-token.png differ diff --git a/docs/images/integrations-supabase.png b/docs/images/integrations-supabase.png new file mode 100644 index 000000000..60808a232 Binary files /dev/null and b/docs/images/integrations-supabase.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png index 234a842b3..fc6593143 100644 Binary files a/docs/images/integrations.png and b/docs/images/integrations.png differ diff --git a/docs/images/organization-ic.png b/docs/images/organization-ic.png index d94f4061a..e45ad2d82 100644 Binary files a/docs/images/organization-ic.png and b/docs/images/organization-ic.png differ diff --git a/docs/images/organization-members.png b/docs/images/organization-members.png index 321808d51..90344b698 100644 Binary files a/docs/images/organization-members.png and b/docs/images/organization-members.png differ diff --git a/docs/images/organization-service-accounts.png b/docs/images/organization-service-accounts.png new file mode 100644 index 000000000..f48e848d3 Binary files /dev/null and b/docs/images/organization-service-accounts.png differ diff --git a/docs/images/organization.png b/docs/images/organization.png index 473c31db0..488d6c4cf 100644 Binary files a/docs/images/organization.png and b/docs/images/organization.png differ diff --git a/docs/images/pit-commits.png b/docs/images/pit-commits.png index 19cfa4976..22599311a 100644 Binary files a/docs/images/pit-commits.png and b/docs/images/pit-commits.png differ diff --git a/docs/images/pit-snapshot.png b/docs/images/pit-snapshot.png index 7e790e875..b7e913108 100644 Binary files a/docs/images/pit-snapshot.png and b/docs/images/pit-snapshot.png differ diff --git a/docs/images/pit-snapshots.png b/docs/images/pit-snapshots.png index f22231648..aa1cd52d3 100644 Binary files a/docs/images/pit-snapshots.png and b/docs/images/pit-snapshots.png differ diff --git a/docs/images/project-download.png b/docs/images/project-download.png index 8750b6cd3..00b02d9bf 100644 Binary files a/docs/images/project-download.png and b/docs/images/project-download.png differ diff --git a/docs/images/project-drag-drop.png b/docs/images/project-drag-drop.png index a283faec6..7ae0ed718 100644 Binary files a/docs/images/project-drag-drop.png and b/docs/images/project-drag-drop.png differ diff --git a/docs/images/project-envar-override.png b/docs/images/project-envar-override.png new file mode 100644 index 000000000..d9e01c8fd Binary files /dev/null and b/docs/images/project-envar-override.png differ diff --git a/docs/images/project-envar-toggle-open.png b/docs/images/project-envar-toggle-open.png deleted file mode 100644 index 297299454..000000000 Binary files a/docs/images/project-envar-toggle-open.png and /dev/null differ diff --git a/docs/images/project-environment.png b/docs/images/project-environment.png index 5b316511c..e2e0d2959 100644 Binary files a/docs/images/project-environment.png and b/docs/images/project-environment.png differ diff --git a/docs/images/project-hide.png b/docs/images/project-hide.png index 69fdb13f0..61aed0bee 100644 Binary files a/docs/images/project-hide.png and b/docs/images/project-hide.png differ diff --git a/docs/images/project-quickstart.png b/docs/images/project-quickstart.png index 6d8d7f664..9a9660d1d 100644 Binary files a/docs/images/project-quickstart.png and b/docs/images/project-quickstart.png differ diff --git a/docs/images/project-search.png b/docs/images/project-search.png index 7388b34ce..bafc0b2c3 100644 Binary files a/docs/images/project-search.png and b/docs/images/project-search.png differ diff --git a/docs/images/project-sort.png b/docs/images/project-sort.png deleted file mode 100644 index 134adc8d5..000000000 Binary files a/docs/images/project-sort.png and /dev/null differ diff --git a/docs/images/secret-versioning.png b/docs/images/secret-versioning.png index ec1734289..96dbfde41 100644 Binary files a/docs/images/secret-versioning.png and b/docs/images/secret-versioning.png differ diff --git a/docs/integrations/cicd/circleci.mdx b/docs/integrations/cicd/circleci.mdx index 7b9298f4b..58b8ae3ff 100644 --- a/docs/integrations/cicd/circleci.mdx +++ b/docs/integrations/cicd/circleci.mdx @@ -1,6 +1,6 @@ --- title: "CircleCI" -description: "How to automatically sync secrets from Infisical into your CircleCI project." +description: "How to sync secrets from Infisical to CircleCI" --- Prerequisites: diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx index 8f93d8162..5ecf68d03 100644 --- a/docs/integrations/cicd/githubactions.mdx +++ b/docs/integrations/cicd/githubactions.mdx @@ -1,6 +1,6 @@ --- title: "GitHub Actions" -description: "How to automatically sync secrets from Infisical into your GitHub Actions." +description: "How to sync secrets from Infisical to GitHub Actions" --- diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index f52afe99f..8a5432272 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -1,6 +1,6 @@ --- title: "GitLab" -description: "How to automatically sync secrets from Infisical into GitLab." +description: "How to sync secrets from Infisical to GitLab" --- Prerequisites: diff --git a/docs/integrations/cicd/travisci.mdx b/docs/integrations/cicd/travisci.mdx index 5509ffc7a..79737a2d8 100644 --- a/docs/integrations/cicd/travisci.mdx +++ b/docs/integrations/cicd/travisci.mdx @@ -1,6 +1,6 @@ --- title: "Travis CI" -description: "How to automatically sync secrets from Infisical to your Travis CI repository." +description: "How to sync secrets from Infisical to Travis CI" --- Prerequisites: diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx index 1bb6c40c9..63fd1e1c6 100644 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ b/docs/integrations/cloud/aws-parameter-store.mdx @@ -1,6 +1,6 @@ --- title: "AWS Parameter Store" -description: "How to automatically sync secrets from Infisical to your AWS Parameter Store." +description: "How to sync secrets from Infisical to AWS Parameter Store" --- Prerequisites: diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 9b282c527..a798ae820 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -1,6 +1,6 @@ --- title: "AWS Secret Manager" -description: "How to automatically sync secrets from Infisical to your AWS Secret Manager." +description: "How to sync secrets from Infisical to AWS Secret Manager" --- Prerequisites: diff --git a/docs/integrations/cloud/azure-key-vault.mdx b/docs/integrations/cloud/azure-key-vault.mdx index 3b23f4d52..90c630666 100644 --- a/docs/integrations/cloud/azure-key-vault.mdx +++ b/docs/integrations/cloud/azure-key-vault.mdx @@ -1,6 +1,6 @@ --- title: "Azure Key Vault" -description: "How to automatically sync secrets from Infisical into your Azure Key Vault." +description: "How to sync secrets from Infisical to Azure Key Vault" --- Prerequisites: diff --git a/docs/integrations/cloud/flyio.mdx b/docs/integrations/cloud/flyio.mdx index 2a1dd7e2c..283c54176 100644 --- a/docs/integrations/cloud/flyio.mdx +++ b/docs/integrations/cloud/flyio.mdx @@ -1,6 +1,6 @@ --- title: "Fly.io" -description: "How to automatically sync secrets from Infisical into your Fly.io project." +description: "How to sync secrets from Infisical to Fly.io" --- Prerequisites: @@ -11,7 +11,7 @@ Prerequisites: ![integrations](../../images/integrations.png) -## Authorize Infisical for Fly.io +## Enter your Fly.io Access Token Obtain a Fly.io access token in Access Tokens diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index 9d0c26789..f3c58d27e 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -1,6 +1,6 @@ --- title: "Heroku" -description: "How to automatically sync secrets from Infisical into your Heroku project." +description: "How to sync secrets from Infisical to Heroku" --- Prerequisites: diff --git a/docs/integrations/cloud/netlify.mdx b/docs/integrations/cloud/netlify.mdx index b6dc36ba0..acd037417 100644 --- a/docs/integrations/cloud/netlify.mdx +++ b/docs/integrations/cloud/netlify.mdx @@ -1,6 +1,6 @@ --- title: "Netlify" -description: "How to automatically sync secrets from Infisical into your Netlify project." +description: "How to sync secrets from Infisical to Netlify" --- diff --git a/docs/integrations/cloud/railway.mdx b/docs/integrations/cloud/railway.mdx index 5ebf8fc1d..e19cfb626 100644 --- a/docs/integrations/cloud/railway.mdx +++ b/docs/integrations/cloud/railway.mdx @@ -1,6 +1,6 @@ --- title: "Railway" -description: "How to automatically sync secrets from Infisical into your Railway projects and services" +description: "How to sync secrets from Infisical to Railway" --- Prerequisites: diff --git a/docs/integrations/cloud/render.mdx b/docs/integrations/cloud/render.mdx index 95890e92a..290e39012 100644 --- a/docs/integrations/cloud/render.mdx +++ b/docs/integrations/cloud/render.mdx @@ -1,6 +1,6 @@ --- title: "Render" -description: "How to automatically sync secrets from Infisical into your Render project." +description: "How to sync secrets from Infisical to Render" --- Prerequisites: diff --git a/docs/integrations/cloud/supabase.mdx b/docs/integrations/cloud/supabase.mdx new file mode 100644 index 000000000..14c787f75 --- /dev/null +++ b/docs/integrations/cloud/supabase.mdx @@ -0,0 +1,44 @@ +--- +title: "Supabase" +description: "How to sync secrets from Infisical to Supabase" +--- + + + The Supabase integration is useful if your Supabase project uses sensitive-information such as [environment variables in edge functions](https://supabase.com/docs/guides/functions/secrets). + + Synced envars can be accessed in edge functions using Deno's built-in handler: `Deno.env.get(MY_SECRET_NAME)`. + + +Prerequisites: + +- Have an account and project set up at [Supabase](https://supabase.com/) +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Enter your Supabase Access Token + +Obtain a Supabase Access Token in your Supabase [Account > Access Tokens](https://app.supabase.com/account/tokens). +![integrations supabase dashboard](../../images/integrations-supabase-dashboard.png) +![integrations supabase token](../../images/integrations-supabase-token.png) + +Press on the Supabase tile and input your Supabase Access Token to grant Infisical access to your Supabase account. + +![integrations supabase authorization](../../images/integrations-supabase-authorization.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Supabase project. Lastly, press create integration to start syncing secrets to Supabase. + +![integrations supabase create](../../images/integrations-supabase-create.png) + +![integrations supabase](../../images/integrations-supabase.png) \ No newline at end of file diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx index af20c60cf..fb265ec98 100644 --- a/docs/integrations/cloud/vercel.mdx +++ b/docs/integrations/cloud/vercel.mdx @@ -1,6 +1,6 @@ --- title: "Vercel" -description: "How to automatically sync secrets from Infisical into your Vercel project." +description: "How to sync secrets from Infisical to Vercel" --- Prerequisites: diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index e6e2622cd..dcbdf44b8 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -18,8 +18,9 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [Vercel](/integrations/cloud/vercel) | Cloud | Available | | [Netlify](/integrations/cloud/netlify) | Cloud | Available | | [Render](/integrations/cloud/render) | Cloud | Available | -| [Railway](/integrations/cloud/railway) | Cloud | Available | +| [Railway](/integrations/cloud/railway) | Cloud | Available | | [Fly.io](/integrations/cloud/flyio) | Cloud | Available | +| [Supabase](/integrations/cloud/supabase) | Cloud | Available | | [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | | [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | | [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | @@ -42,7 +43,5 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [Flask](/integrations/frameworks/flask) | Framework | Available | | [Laravel](/integrations/frameworks/laravel) | Framework | Available | | [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | -| GCP | Cloud | Coming soon | -| DigitalOcean | Cloud | Coming soon | -| GitHub Actions | CI/CD | Coming soon | +| GCP Secret Manager | Cloud | Coming soon | | Jenkins | CI/CD | Coming soon | diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 959a4fce2..5ea3967d9 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -1,6 +1,6 @@ --- title: 'Kubernetes' -description: "This page explains how to use Infisical to inject secrets into Kubernetes clusters." +description: "How to use Infisical to inject secrets into Kubernetes clusters." --- ![title](../../images/k8-diagram.png) diff --git a/docs/mint.json b/docs/mint.json index 165ce6862..850809a4f 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -151,6 +151,7 @@ "integrations/cloud/render", "integrations/cloud/railway", "integrations/cloud/flyio", + "integrations/cloud/supabase", "integrations/cloud/azure-key-vault", "integrations/cicd/githubactions", "integrations/cicd/gitlab", diff --git a/frontend/src/pages/api/integrations/DeleteIntegration.ts b/frontend/src/pages/api/integrations/DeleteIntegration.ts index a16c629a3..ae401984c 100644 --- a/frontend/src/pages/api/integrations/DeleteIntegration.ts +++ b/frontend/src/pages/api/integrations/DeleteIntegration.ts @@ -19,7 +19,6 @@ const deleteIntegration = ({ integrationId }: Props) => if (res && res.status === 200) { return (await res.json()).integration; } - console.log('Failed to delete an integration'); return undefined; });