Add service account support for organization endpoints and update docs images

This commit is contained in:
Tuan Dang
2023-04-10 17:30:51 +03:00
parent afa7b35d50
commit 5032450b1c
34 changed files with 189 additions and 57 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
};

View File

@@ -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();

View File

@@ -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) => {

View File

@@ -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();
};

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -53,7 +53,7 @@ router.post(
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
location: 'body'
locationOrganizationId: 'body'
}),
serviceAccountsController.createServiceAccount
);

View File

@@ -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
![API key dashboard](../../images/api-key-dashboard.png)
![API key in personal settings](../../images/api-key-settings.png)
</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.
![token add](../../images/project-token-add.png)
</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.

View File

@@ -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

View File

@@ -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.
![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.

View File

@@ -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.

View File

@@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 717 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB