Add service account support for organization endpoints and update docs images
@@ -22,8 +22,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,
|
||||
|
||||
@@ -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,30 +43,62 @@ 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 resource'
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -228,6 +268,7 @@ const updateSubscriptionOrgQuantity = async ({
|
||||
};
|
||||
|
||||
export {
|
||||
validateClientForOrganization,
|
||||
createOrganization,
|
||||
initSubscriptionOrg,
|
||||
updateSubscriptionOrgQuantity
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ServiceTokenData,
|
||||
IServiceTokenData,
|
||||
ISecret,
|
||||
IOrganization,
|
||||
ServiceAccountWorkspacePermission
|
||||
} from '../models';
|
||||
import {
|
||||
@@ -109,9 +110,6 @@ 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) {
|
||||
const permission = await ServiceAccountWorkspacePermission.findOne({
|
||||
serviceAccount,
|
||||
@@ -123,7 +121,6 @@ const validateClientForServiceAccount = async ({
|
||||
message: 'Failed service account authorization for the given workspace environment'
|
||||
});
|
||||
|
||||
// TODO: refactor
|
||||
let runningIsDisallowed = false;
|
||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||
switch (requiredPermission) {
|
||||
@@ -180,7 +177,6 @@ const validateClientForServiceAccount = async ({
|
||||
});
|
||||
|
||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||
// TODO: refactor
|
||||
let runningIsDisallowed = false;
|
||||
requiredPermissions?.forEach((requiredPermission: string) => {
|
||||
switch (requiredPermission) {
|
||||
@@ -202,9 +198,6 @@ const validateClientForServiceAccount = async ({
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,9 +224,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
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
ISecret,
|
||||
IServiceAccount,
|
||||
User,
|
||||
Membership
|
||||
Membership,
|
||||
IOrganization,
|
||||
Organization,
|
||||
} from '../models';
|
||||
import { sendMail } from './nodemailer';
|
||||
import { validateMembership } from './membership';
|
||||
@@ -288,11 +290,39 @@ 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
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,8 +20,8 @@ const requireMembershipOrgAuth = ({
|
||||
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) => {
|
||||
|
||||
@@ -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,31 @@ 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];
|
||||
|
||||
// TODO: incorporate [acceptedRoles] and [acceptedStatuses]
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,7 +53,7 @@ router.post(
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
location: 'body'
|
||||
locationOrganizationId: 'body'
|
||||
}),
|
||||
serviceAccountsController.createServiceAccount
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="API Key">
|
||||
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
|
||||
|
||||

|
||||

|
||||
</Accordion>
|
||||
<Accordion title="Service Account">
|
||||
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 <access_key>`.
|
||||
|
||||
You can create a Service Account in Organization Settings > Service Accounts
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Infisical Token">
|
||||
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.<rest_of_your_infisical_token>`.
|
||||
|
||||
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 <infisical_token>`.
|
||||
|
||||
You can obtain an Infisical Token in Project Settings > Service Tokens.
|
||||
|
||||

|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</AccordionGroup>
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
## Concepts
|
||||
|
||||
|
||||
@@ -24,6 +24,14 @@ To add a member to your organization, scroll down to the "Organization Members"
|
||||
projects by default.
|
||||
</Note>
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## Incident contacts
|
||||
|
||||
Incident contacts of an organization are alerted if anything abnormal is detected within the operations of an organization.
|
||||
|
||||
@@ -25,16 +25,16 @@ In most cases, environment variables belong to specific environments: developmen
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||

|
||||
|
||||
### Search
|
||||
|
||||
@@ -42,12 +42,6 @@ You can search for any environment variable by its key.
|
||||
|
||||

|
||||
|
||||
### Sort
|
||||
|
||||
You can sort environment variables alphabetically by their keys.
|
||||
|
||||

|
||||
|
||||
### Hide/Un-hide
|
||||
|
||||
You can hide or un-hide the values of your environment variables. By default, the values are hidden for your privacy.
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 744 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 717 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 622 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 636 KiB |
BIN
docs/images/organization-service-accounts.png
Normal file
|
After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 727 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 669 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 680 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 1024 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 225 KiB |
BIN
docs/images/project-envar-override.png
Normal file
|
After Width: | Height: | Size: 686 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 718 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 1.0 MiB |