Merge pull request #411 from Infisical/revised-service-token-docs

Add read/write support for service tokens and update CRUD examples in docs to use service tokens
This commit is contained in:
BlackMagiq
2023-03-07 15:42:07 +07:00
committed by GitHub
26 changed files with 1807 additions and 570 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -953,6 +953,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
}
*/
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);

View File

@@ -17,7 +17,35 @@ import { ABILITY_READ } from '../../variables/organization';
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
export const getServiceTokenData = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"serviceTokenData": {
"type": "object",
$ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token"
}
}
}
}
}
}
*/
return res.status(200).json(req.serviceTokenData);
}
/**
* Create new service token data for workspace with id [workspaceId] and
@@ -28,6 +56,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData;
try {
const {
name,
@@ -36,7 +65,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
encryptedKey,
iv,
tag,
expiresIn
expiresIn,
permissions
} = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
@@ -59,7 +89,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
secretHash,
encryptedKey,
iv,
tag
tag,
permissions
}).save();
// return service token data without sensitive data

View File

@@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import {
IUser,
User,
ServiceTokenData,
APIKeyData
@@ -148,7 +149,10 @@ const getAuthSTDPayload = async ({
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag').populate('user');
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} catch (err) {
throw UnauthorizedRequestError({

View File

@@ -1,12 +1,14 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { User, ServiceTokenData } from '../models';
import {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -25,9 +27,11 @@ declare module 'jsonwebtoken' {
* @returns
*/
const requireAuth = ({
acceptedAuthModes = ['jwt']
acceptedAuthModes = ['jwt'],
requiredServiceTokenPermissions = []
}: {
acceptedAuthModes: string[];
requiredServiceTokenPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
@@ -38,11 +42,22 @@ const requireAuth = ({
});
// attach auth payloads
let serviceTokenData: any;
switch (authTokenType) {
case 'serviceToken':
req.serviceTokenData = await getAuthSTDPayload({
serviceTokenData = await getAuthSTDPayload({
authTokenValue
});
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});
req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;
break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({

View File

@@ -23,7 +23,7 @@ const requireSecretsAuth = ({
// case: validate 1 secret
secrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds: req.body.secrets.id
secretIds: [req.body.secrets.id]
});
} else if (Array.isArray(req.body.secretIds)) {
secrets = await validateSecrets({

View File

@@ -21,7 +21,7 @@ const requireWorkspaceAuth = ({
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { workspaceId } = req[location];
if (req.user) {
// case: jwt auth
const membership = await validateMembership({
@@ -32,11 +32,11 @@ const requireWorkspaceAuth = ({
req.membership = membership;
}
if (
req.serviceTokenData
&& req.serviceTokenData.workspace !== workspaceId
&& req.serviceTokenData.environment !== req.query.environment
&& req.serviceTokenData.environment !== req.body.environment
) {
next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
}

View File

@@ -3,13 +3,14 @@ import { Schema, model, Types } from 'mongoose';
export interface IServiceTokenData {
name: string;
workspace: Types.ObjectId;
environment: string; // TODO: adapt to upcoming environment id
environment: string;
user: Types.ObjectId;
expiresAt: Date;
secretHash: string;
encryptedKey: string;
iv: string;
tag: string;
permissions: string[];
}
const serviceTokenDataSchema = new Schema<IServiceTokenData>(
@@ -51,6 +52,11 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
tag: {
type: String,
select: false
},
permissions: {
type: [String],
enum: ['read', 'write'],
default: ['read']
}
},
{

View File

@@ -22,7 +22,8 @@ import {
router.post(
'/batch',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read', 'write']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -99,7 +100,8 @@ router.post(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -115,7 +117,8 @@ router.get(
query('tagSlugs'),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['read']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -154,7 +157,8 @@ router.patch(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
@@ -182,7 +186,8 @@ router.delete(
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'],
requiredServiceTokenPermissions: ['write']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
@@ -192,5 +197,3 @@ router.delete(
export default router;

View File

@@ -30,13 +30,22 @@ router.post(
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('name').exists().trim(),
body('workspaceId'),
body('environment'),
body('encryptedKey'),
body('iv'),
body('tag'),
body('expiresIn'), // measured in ms
body('name').exists().isString().trim(),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('encryptedKey').exists().isString().trim(),
body('iv').exists().isString().trim(),
body('tag').exists().isString().trim(),
body('expiresIn').exists().isNumeric(), // measured in ms
body('permissions').isArray({ min: 1 }).custom((value: string[]) => {
const allowedPermissions = ['read', 'write'];
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
if (invalidValues.length > 0) {
throw new Error(`permissions contains invalid values: ${invalidValues.join(', ')}`);
}
return true
}),
validateRequest,
serviceTokenDataController.createServiceTokenData
);

View File

@@ -197,6 +197,23 @@ const generateOpenAPISpec = async () => {
secretValueCiphertext: '',
secretValueIV: '',
secretValueTag: '',
},
ServiceTokenData: {
_id: '',
name: '',
workspace: '',
environment: '',
user: {
_id: '',
firstName: '',
lastName: ''
},
expiresAt: '2023-01-13T14:16:12.210Z',
encryptedKey: '',
iv: '',
tag: '',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
}
}
};

View File

@@ -0,0 +1,4 @@
---
title: "Get"
openapi: "GET /api/v2/service-token/"
---

View File

@@ -2,14 +2,24 @@
title: "Authentication"
---
To authenticate requests with Infisical, 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
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.
<AccordionGroup>
<Accordion title="API Key">
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)
![Adding an API key](../../images/api-key-add.png)
</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>`.
<Info>
It's important to keep your API key secure, as it grants access to your
secrets in Infisical. For added security, set a reasonable expiration time and
rotate your API key on a regular basis.
</Info>
You can obtain an Infisical Token in Project Settings > Service Tokens.
![token add](../../images/project-token-add.png)
</Accordion>
</AccordionGroup>

View File

@@ -2,49 +2,48 @@
title: "Create secrets"
---
In this example, we demonstrate how to add secrets to a project and environment.
In this example, we demonstrate how to add secrets to a project and environment using an Infisical Token.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Flow
1. [Get your (encrypted) private key](/api-reference/endpoints/users/me).
2. Decrypt your (encrypted) private key with your password.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project key.
6. [Send (encrypted) secret(s) to the Infical API](/api-reference/endpoints/secrets/create)
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. Decrypt the (encrypted) project key with the key from your Infisical Token.
3. Encrypt your secret(s) with the project key
4. [Send (encrypted) secret(s) to Infical](/api-reference/endpoints/secrets/create)
## Example
<Tabs>
<Tab title="Javascript">
```js
const crypto = require('crypto');
const axios = require('axios');
const nacl = require('tweetnacl');
const BASE_URL = 'https://app.infisical.com';
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = (
text,
secret
) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
const encrypt = ({ text, secret }) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = (ciphertext, iv, tag, secret) => {
const decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
@@ -59,95 +58,96 @@ const decrypt = (ciphertext, iv, tag, secret) => {
}
const createSecrets = async () => {
const API_KEY = 'your_api_key';
const PSWD = 'your_pswd';
const WORKSPACE_ID = 'your_workspace_id';
const serviceToken = '';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
const secretType = 'shared'; // 'shared' or 'personal'
const secretKey = 'some_key';
const secretValue = 'some_value';
const secretComment = 'some_comment';
const SECRET_KEY = 'SOME_KEY';
const SECRET_VALUE = 'SOME_VALUE';
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 1. get (encrypted) private key
const user = await axios.get(
'https://api.infisical.com/api/v2/users/me', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
const projectKey = decrypt({
ciphertext: serviceTokenData.encryptedKey,
iv: serviceTokenData.iv,
tag: serviceTokenData.tag,
secret: serviceTokenSecret
});
// 3. Encrypt your secret(s) with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt({
text: secretKey,
secret: projectKey
});
// 2. decrypt your (encrypted) private key with your password
const privateKey = decrypt({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
secret: PSWD.slice(0, 32).padStart(32, '0');
});
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt({
text: secretValue,
secret: projectKey
});
// 3. get the (encrypted) project key for the project
const encryptedProjectKey = await axios.get(
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
headers: {
'X-API-KEY': API_KEY
}
}
);
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encrypt({
text: secretComment,
secret: projectKey
});
const secret = {
type: secretType,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
// 4. decrypt the project key with your private key
const projectKey = nacl.box.open(
util.decodeBase64(encryptedProjectKey),
util.decodeBase64(encryptedProjectKey.nonce),
util.decodeBase64(encryptedProjectKey.sender.publicKey),
util.decodeBase64(PSWD)
);
// 5. encrypt your secret(s) with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt(SECRET_KEY, projectKey);
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt(SECRET_VALUE, projectKey);
const secret = {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
}
// 6. Send (encrypted) secret(s) to the Infisical API
await axios.post(
`https://api.infisical.com/api/v2/secrets`,
{
workspaceId: WORKSPACE_ID,
environment: 'dev',
secrets: secret
},
{
headers: {
'X-API-KEY': API_KEY
}
}
);
// 4. Send (encrypted) secret(s) to Infisical
await axios.post(
`${BASE_URL}/api/v2/secrets`,
{
workspaceId: serviceTokenData.workspace,
environment: serviceTokenData.environment,
secrets: [secret]
},
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
}
createSecrets();
```
</Tab>
</Tabs>
<Info>
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
are ports of NaCl available in every major language.
</Info>
<Tip>
It can be useful to perform steps 1-4 ahead of time and store away your
private key (and even project key) for later use. The Infisical CLI works by
securely storing your private key via your OS keyring.
</Tip>
</Info>

View File

@@ -7,24 +7,32 @@ In this example, we demonstrate how to delete secrets
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Create either an [API Key](/api-reference/overview/authentication) or [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Example
<Tabs>
<Tab title="Javascript">
</Tab>
</Tabs>
```js
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const deleteSecrets = async () => {
const API_KEY = "your_api_key";
const SECRET_ID = "ID"; // ID of secret to delete
const serviceToken = 'your_service_token';
const secretId = 'id_of_secret_to_delete';
// 6. Send ID(s) of secret(s) to delete to the Infisical API
await axios.delete(
`https://api.infisical.com/api/v2/secrets`,
`${BASE_URL}/api/v2/secrets`,
{
secretIds: SECRET_ID,
secretIds: [secretId],
},
{
headers: {
"X-API-KEY": API_KEY,
Authorization: `Bearer ${serviceToken}`
},
}
);
@@ -32,3 +40,8 @@ const deleteSecrets = async () => {
deleteSecrets();
```
<Info>
If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header.
</Info>

View File

@@ -2,48 +2,33 @@
title: "Retrieve secrets"
---
In this example, we demonstrate how to retrieve secrets from a project and environment.
In this example, we demonstrate how to retrieve secrets from a project and environment using an Infisical Token.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Flow
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
2. Decrypt your (encrypted) private key with your password.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. [Get secrets for a project and environment.](/api-reference/endpoints/secrets/read)
6. Decrypt the (encrypted) secrets
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. [Get secrets for your project and environment](/api-reference/endpoints/secrets/read).
3. Decrypt the (encrypted) project key with the key from your Infisical Token.
4. Decrypt the (encrypted) secrets
## Example
<Tabs>
<Tab title="Javascript">
```js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = (
text,
secret
) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = (ciphertext, iv, tag, secret) => {
const decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
@@ -51,73 +36,63 @@ const decrypt = (ciphertext, iv, tag, secret) => {
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
cleartext += decipher.final('utf8');
return cleartext;
return cleartext;
}
const retrieveSecrets = async () => {
const API_KEY = 'your_api_key';
const PSWD = 'your_pswd';
const WORKSPACE_ID = 'your_workspace_id';
const getSecrets = async () => {
const serviceToken = 'your_service_token';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
// 1. get (encrypted) private key
const user = await axios.get(
'https://api.infisical.com/api/v2/users/me', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 2. decrypt your (encrypted) private key with your password
const privateKey = decrypt({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
secret: PSWD.slice(0, 32).padStart(32, '0');
});
// 2. Get secrets for your project and environment
const { data } = await axios.get(
`${BASE_URL}/api/v2/secrets?${new URLSearchParams({
environment: serviceTokenData.environment,
workspaceId: serviceTokenData.workspace
})}`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 3. get the (encrypted) project key for the project
const encryptedProjectKey = await axios.get(
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
headers: {
'X-API-KEY': API_KEY
}
}
);
const encryptedSecrets = data.secrets;
// 4. decrypt the project key with your private key
const projectKey = nacl.box.open(
util.decodeBase64(encryptedProjectKey),
util.decodeBase64(projectKey.nonce),
util.decodeBase64(projectKey.sender.publicKey),
util.decodeBase64(privateKey)
);
// 3. Decrypt the (encrypted) project key with the key from your Infisical Token
const projectKey = decrypt({
ciphertext: serviceTokenData.encryptedKey,
iv: serviceTokenData.iv,
tag: serviceTokenData.tag,
secret: serviceTokenSecret
});
// 5. get (encrypted) secrets for a project and environment.
const encryptedSecrets = await axios.get(
'https://api.infisical.com/api/v2/secrets', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 6. decrypt the (encrypted) secrets
const secrets = encryptedSecrets.map((encryptedSecret) => {
// 4. Decrypt the (encrypted) secrets
const secrets = encryptedSecrets.map((secret) => {
const secretKey = decrypt({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag
secret: projectKey
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
secret: projectKey
});
const secretValue = decrypt({
ciphertext: encryptedSecret.secretValueCiphertext,
iv: encryptedSecret.secretValueIV,
tag: encryptedSecret.secretValueTag
secret: projectKey
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
secret: projectKey
});
return ({
@@ -125,18 +100,18 @@ const retrieveSecrets = async () => {
secretValue
});
});
console.log('secrets: ', secrets);
}
retrieveSecrets();
getSecrets();
```
</Tab>
</Tabs>
<Info>
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
are ports of NaCl available in every major language.
</Info>
<Tip>
It can be useful to perform steps 1-4 ahead of time and store away your
private key (and even project key) for later use. The Infisical CLI works by
securely storing your private key via your OS keyring.
</Tip>
</Info>

View File

@@ -2,48 +2,47 @@
title: "Update secrets"
---
In this example, we demonstrate how to update secrets
In this example, we demonstrate how to update secrets using an Infisical Token.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Create an [Infisical Token](../../../getting-started/dashboard/token) for your project and environment.
- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction).
## Flow
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
2. Decrypt your (encrypted) private key with your password.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project key.
6. [Send (encrypted) updated secret(s) to the Infical API.](/api-reference/endpoints/secrets/update)
1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key.
2. Decrypt the (encrypted) project key with the key from your Infisical Token.
3. Encrypt your updated secret(s) with the project key
4. [Send (encrypted) updated secret(s) to Infical](/api-reference/endpoints/secrets/update)
## Example
<Tabs>
<Tab title="Javascript">
```js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://app.infisical.com';
const ALGORITHM = 'aes-256-gcm';
const BLOCK_SIZE_BYTES = 16;
const encrypt = (
text,
secret
) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
const encrypt = ({ text, secret }) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
let ciphertext = cipher.update(text, 'utf8', 'base64');
ciphertext += cipher.final('base64');
return {
ciphertext,
iv: iv.toString('base64'),
tag: cipher.getAuthTag().toString('base64')
};
}
const decrypt = (ciphertext, iv, tag, secret) => {
const decrypt = ({ ciphertext, iv, tag, secret}) => {
const decipher = crypto.createDecipheriv(
ALGORITHM,
secret,
@@ -58,95 +57,96 @@ const decrypt = (ciphertext, iv, tag, secret) => {
}
const updateSecrets = async () => {
const API_KEY = 'your_api_key';
const PSWD = 'your_pswd';
const WORKSPACE_ID = 'your_workspace_id';
const serviceToken = 'your_service_token';
const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1);
const secretId = 'id_of_secret_to_update';
const secretKey = 'some_key';
const secretValue = 'updated_value';
const secretComment = 'updated_comment';
const SECRET_ID = 'ID' // ID of secret to update
const SECRET_KEY = 'SOME_KEY';
const SECRET_VALUE = 'SOME_VALUE';
// 1. Get your Infisical Token data
const { data: serviceTokenData } = await axios.get(
`${BASE_URL}/api/v2/service-token`,
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
// 1. get (encrypted) private key
const user = await axios.get(
'https://api.infisical.com/api/v2/users/me', {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 2. Decrypt the (encrypted) project key with the key from your Infisical Token
const projectKey = decrypt({
ciphertext: serviceTokenData.encryptedKey,
iv: serviceTokenData.iv,
tag: serviceTokenData.tag,
secret: serviceTokenSecret
});
// 3. Encrypt your updated secret(s) with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt({
text: secretKey,
secret: projectKey
});
// 2. decrypt your (encrypted) private key with your password
const privateKey = decrypt({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
secret: PSWD.slice(0, 32).padStart(32, '0');
});
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt({
text: secretValue,
secret: projectKey
});
// 3. get the (encrypted) project key for the project
const encryptedProjectKey = await axios.get(
`https://api.infisical.com/api/v2/workspace/${WORKSPACE_ID}`, {
headers: {
'X-API-KEY': API_KEY
}
}
);
// 4. decrypt the project key with your private key
const projectKey = nacl.box.open(
util.decodeBase64(encryptedProjectKey),
util.decodeBase64(projectKey.nonce),
util.decodeBase64(projectKey.sender.publicKey),
util.decodeBase64(privateKey)
);
// 5. encrypt your secret(s) with the project key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encrypt(SECRET_KEY, projectKey);
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encrypt(SECRET_VALUE, projectKey);
const secret = {
id: SECRET_ID,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
}
// 6. Send (encrypted) secret(s) to the Infisical API
await axios.patch(
`https://api.infisical.com/api/v2/secrets`,
{
secrets: secret
},
{
headers: {
'X-API-KEY': API_KEY
}
}
);
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encrypt({
text: secretComment,
secret: projectKey
});
const secret = {
id: secretId,
workspace: serviceTokenData.workspace,
environment: serviceTokenData.environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
// 4. Send (encrypted) updated secret(s) to Infisical
await axios.patch(
`${BASE_URL}/api/v2/secrets`,
{
secrets: [secret]
},
{
headers: {
Authorization: `Bearer ${serviceToken}`
}
}
);
}
updateSecrets();
```
</Tab>
</Tabs>
<Info>
This example uses [TweetNaCl.js](https://tweetnacl.js.org/#/), a port of
TweetNacl/Nacl, to perform asymmeric decryption of the project key but there
are ports of NaCl available in every major language.
</Info>
<Tip>
It can be useful to perform steps 1-4 ahead of time and store away your
private key (and even project key) for later use. The Infisical CLI works by
securely storing your private key via your OS keyring.
</Tip>
</Info>

View File

@@ -12,10 +12,11 @@ With the REST API, users can create, read, update, and delete secrets, as well a
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process.
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by protected keys that are encrypted by keys derived from Argon2id applied to the user's password before being sent off to the server during the account signup process.
- Each (encrypted) secret belongs to a project and environment.
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
- Infisical Tokens contain a symmetric key that can be used to decrypt a copy of a project key from the [call to get the Infisical Token data](/api-reference/endpoints/service-tokens/get).
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
<Info>

View File

@@ -249,6 +249,12 @@
"api-reference/endpoints/secrets/versions",
"api-reference/endpoints/secrets/rollback-version"
]
},
{
"group": "Service Tokens",
"pages": [
"api-reference/endpoints/service-tokens/get"
]
}
]
},

View File

@@ -309,78 +309,6 @@ paths:
example: any
code:
example: any
/api/v1/signup/complete-account/signup:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
'403':
description: Forbidden
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
firstName:
example: any
lastName:
example: any
publicKey:
example: any
encryptedPrivateKey:
example: any
iv:
example: any
tag:
example: any
salt:
example: any
verifier:
example: any
organizationName:
example: any
/api/v1/signup/complete-account/invite:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
'403':
description: Forbidden
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
firstName:
example: any
lastName:
example: any
publicKey:
example: any
encryptedPrivateKey:
example: any
iv:
example: any
tag:
example: any
salt:
example: any
verifier:
example: any
/api/v1/auth/token:
post:
description: ''
@@ -412,7 +340,11 @@ paths:
/api/v1/auth/login2:
post:
description: ''
parameters: []
parameters:
- name: user-agent
in: header
schema:
type: string
responses:
'200':
description: OK
@@ -431,7 +363,11 @@ paths:
/api/v1/auth/logout:
post:
description: ''
parameters: []
parameters:
- name: user-agent
in: header
schema:
type: string
responses:
'200':
description: OK
@@ -691,6 +627,18 @@ paths:
description: OK
'400':
description: Bad Request
/api/v1/organization/{organizationId}/workspace-memberships:
get:
description: ''
parameters:
- name: organizationId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v1/workspace/{workspaceId}/keys:
get:
description: ''
@@ -933,6 +881,26 @@ paths:
properties:
role:
example: any
/api/v1/membership/{membershipId}/deny-permissions:
post:
description: ''
parameters:
- name: membershipId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
permissions:
example: any
/api/v1/key/{workspaceId}:
post:
description: ''
@@ -1147,11 +1115,17 @@ paths:
properties:
clientProof:
example: any
protectedKey:
example: any
protectedKeyIV:
example: any
protectedKeyTag:
example: any
encryptedPrivateKey:
example: any
iv:
encryptedPrivateKeyIV:
example: any
tag:
encryptedPrivateKeyTag:
example: any
salt:
example: any
@@ -1247,11 +1221,17 @@ paths:
schema:
type: object
properties:
protectedKey:
example: any
protectedKeyIV:
example: any
protectedKeyTag:
example: any
encryptedPrivateKey:
example: any
iv:
encryptedPrivateKeyIV:
example: any
tag:
encryptedPrivateKeyTag:
example: any
salt:
example: any
@@ -1270,6 +1250,39 @@ paths:
description: OK
'400':
description: Bad Request
/api/v1/integration/:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
integrationAuthId:
example: any
app:
example: any
appId:
example: any
isActive:
example: any
sourceEnvironment:
example: any
targetEnvironment:
example: any
owner:
example: any
path:
example: any
region:
example: any
/api/v1/integration/{integrationId}:
patch:
description: ''
@@ -1290,17 +1303,17 @@ paths:
schema:
type: object
properties:
app:
example: any
environment:
example: any
isActive:
example: any
target:
app:
example: any
context:
appId:
example: any
siteId:
targetEnvironment:
example: any
owner:
example: any
delete:
description: ''
@@ -1322,6 +1335,33 @@ paths:
responses:
'200':
description: OK
/api/v1/integration-auth/{integrationAuthId}:
get:
description: ''
parameters:
- name: integrationAuthId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
'400':
description: Bad Request
delete:
description: ''
parameters:
- name: integrationAuthId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
'400':
description: Bad Request
/api/v1/integration-auth/oauth-token:
post:
description: ''
@@ -1343,6 +1383,29 @@ paths:
example: any
integration:
example: any
/api/v1/integration-auth/access-token:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
workspaceId:
example: any
accessId:
example: any
accessToken:
example: any
integration:
example: any
/api/v1/integration-auth/{integrationAuthId}/apps:
get:
description: ''
@@ -1357,13 +1420,115 @@ paths:
description: OK
'400':
description: Bad Request
/api/v1/integration-auth/{integrationAuthId}:
delete:
/api/v2/signup/complete-account/signup:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
'403':
description: Forbidden
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
firstName:
example: any
lastName:
example: any
protectedKey:
example: any
protectedKeyIV:
example: any
protectedKeyTag:
example: any
publicKey:
example: any
encryptedPrivateKey:
example: any
encryptedPrivateKeyIV:
example: any
encryptedPrivateKeyTag:
example: any
salt:
example: any
verifier:
example: any
organizationName:
example: any
/api/v2/signup/complete-account/invite:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
'403':
description: Forbidden
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
firstName:
example: any
lastName:
example: any
protectedKey:
example: any
protectedKeyIV:
example: any
protectedKeyTag:
example: any
publicKey:
example: any
encryptedPrivateKey:
example: any
encryptedPrivateKeyIV:
example: any
encryptedPrivateKeyTag:
example: any
salt:
example: any
verifier:
example: any
/api/v2/auth/login1:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
clientPublicKey:
example: any
/api/v2/auth/login2:
post:
description: ''
parameters:
- name: integrationAuthId
in: path
required: true
- name: user-agent
in: header
schema:
type: string
responses:
@@ -1371,6 +1536,54 @@ paths:
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
clientProof:
example: any
/api/v2/auth/mfa/send:
post:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
/api/v2/auth/mfa/verify:
post:
description: ''
parameters:
- name: user-agent
in: header
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
example: any
mfaToken:
example: any
/api/v2/users/me:
get:
summary: Retrieve the current user on the request
@@ -1392,6 +1605,23 @@ paths:
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/users/me/mfa:
patch:
description: ''
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
isMfaEnabled:
example: any
/api/v2/users/me/organizations:
get:
summary: Return organizations that current user is part of
@@ -1615,6 +1845,62 @@ paths:
properties:
environmentSlug:
example: any
get:
description: ''
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v2/workspace/{workspaceId}/tags:
get:
description: ''
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
post:
description: ''
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
example: any
slug:
example: any
/api/v2/workspace/tags/{tagId}:
delete:
description: ''
parameters:
- name: tagId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
/api/v2/workspace/{workspaceId}/secrets:
post:
description: ''
@@ -1804,6 +2090,28 @@ paths:
description: Bad Request
security:
- apiKeyAuth: []
/api/v2/workspace/{workspaceId}/auto-capitalization:
patch:
description: ''
parameters:
- name: workspaceId
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
'400':
description: Bad Request
requestBody:
content:
application/json:
schema:
type: object
properties:
autoCapitalization:
example: any
/api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}:
post:
description: ''
@@ -1968,11 +2276,38 @@ paths:
properties:
secret:
example: any
/api/v2/secrets/batch:
post:
description: ''
parameters:
- name: user-agent
in: header
schema:
type: string
responses:
'200':
description: OK
requestBody:
content:
application/json:
schema:
type: object
properties:
workspaceId:
example: any
environment:
example: any
requests:
example: any
/api/v2/secrets/:
post:
summary: Create new secret(s)
description: Create one or many secrets for a given project and environment.
parameters: []
parameters:
- name: user-agent
in: header
schema:
type: string
responses:
'200':
description: OK
@@ -2022,6 +2357,10 @@ paths:
in: query
schema:
type: string
- name: user-agent
in: header
schema:
type: string
- name: content
in: query
schema:
@@ -2073,7 +2412,11 @@ paths:
delete:
summary: Delete secret(s)
description: Delete one or many secrets by their ID(s)
parameters: []
parameters:
- name: user-agent
in: header
schema:
type: string
responses:
'200':
description: OK
@@ -2101,13 +2444,23 @@ paths:
description: ID(s) of secrets - string or array of strings
/api/v2/service-token/:
get:
description: ''
summary: Return Infisical Token data
description: Return Infisical Token data
parameters: []
responses:
'200':
description: OK
'400':
description: Bad Request
content:
application/json:
schema:
type: object
properties:
serviceTokenData:
type: object
$ref: '#/components/schemas/ServiceTokenData'
description: Details of service token
security:
- bearerAuth: []
post:
description: ''
parameters: []
@@ -2136,6 +2489,8 @@ paths:
example: any
expiresIn:
example: any
permissions:
example: any
/api/v2/service-token/{serviceTokenDataId}:
delete:
description: ''
@@ -2317,9 +2672,6 @@ components:
Project:
type: object
properties:
_id:
type: string
example: ''
name:
type: string
example: My Project
@@ -2602,6 +2954,51 @@ components:
secretValueTag:
type: string
example: ''
ServiceTokenData:
type: object
properties:
_id:
type: string
example: ''
name:
type: string
example: ''
workspace:
type: string
example: ''
environment:
type: string
example: ''
user:
type: object
properties:
_id:
type: string
example: ''
firstName:
type: string
example: ''
lastName:
type: string
example: ''
expiresAt:
type: string
example: '2023-01-13T14:16:12.210Z'
encryptedKey:
type: string
example: ''
iv:
type: string
example: ''
tag:
type: string
example: ''
updatedAt:
type: string
example: '2023-01-13T14:16:12.210Z'
createdAt:
type: string
example: '2023-01-13T14:16:12.210Z'
securitySchemes:
bearerAuth:
type: http

View File

@@ -23,4 +23,4 @@ module.exports = {
// strictMode: true,
// serializeConfig: false,
// react: { useSuspense: false }
};
};

View File

@@ -37,7 +37,7 @@ export const useCreateServiceToken = () => {
return useMutation<CreateServiceTokenRes, {}, CreateServiceTokenDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post('/api/v2/service-token/', body);
data.serviceToken += `.${ body.randomBytes}`;
data.serviceToken += `.${body.randomBytes}`;
return data;
},
onSuccess: ({ serviceTokenData: { workspace } }) => {

View File

@@ -19,6 +19,7 @@ export type CreateServiceTokenDTO = {
iv: string;
tag: string;
randomBytes: string;
permissions: string[];
};
export type CreateServiceTokenRes = {

View File

@@ -203,7 +203,7 @@ export const ProjectSettingsPage = () => {
}
};
const onCreateServiceToken = async ({ environment, expiresIn, name }: CreateServiceToken) => {
const onCreateServiceToken = async ({ environment, expiresIn, name, permissions }: CreateServiceToken) => {
// type guard
if (!latestFileKey) return '';
try {
@@ -219,7 +219,7 @@ export const ProjectSettingsPage = () => {
plaintext: key,
key: randomBytes
});
const res = await createServiceToken.mutateAsync({
encryptedKey: ciphertext,
iv,
@@ -228,9 +228,12 @@ export const ProjectSettingsPage = () => {
expiresIn: Number(expiresIn),
name,
workspaceId: workspaceID,
randomBytes
randomBytes,
permissions: Object.entries(permissions)
.filter(([, permissionsValue]) => permissionsValue)
.map(([permissionsKey]) => permissionsKey)
});
console.log(res);
createNotification({
text: 'Successfully created a service token',
type: 'success'

View File

@@ -8,6 +8,7 @@ import * as yup from 'yup';
import {
Button,
Checkbox,
DeleteActionModal,
EmptyState,
FormControl,
@@ -42,7 +43,11 @@ const apiTokenExpiry = [
const createServiceTokenSchema = yup.object({
name: yup.string().required().label('Service Token Name'),
environment: yup.string().required().label('Environment'),
expiresIn: yup.string().required().label('Service Token Name')
expiresIn: yup.string().required().label('Service Token Name'),
permissions: yup.object().shape({
read: yup.boolean().required(),
write: yup.boolean().required()
}).defined().required()
});
export type CreateServiceToken = yup.InferType<typeof createServiceTokenSchema>;
@@ -87,7 +92,7 @@ export const ServiceTokenSection = ({
'createAPIToken',
'deleteAPITokenConfirmation'
] as const);
const {
control,
reset,
@@ -216,6 +221,90 @@ export const ServiceTokenSection = ({
</FormControl>
)}
/>
<Controller
control={control}
name="permissions"
defaultValue={{
read: true,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error }}) => {
const options = [{
label: 'Read (default)',
value: 'read'
}, {
label: 'Write (optional)',
value: 'write'
}];
return (
<FormControl
label="Permissions"
errorText={error?.message}
isError={Boolean(error)}
>
<>
{options.map(({ label, value: optionValue }) => {
// TODO: refactor
return (
<Checkbox
id={value[optionValue]}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
isDisabled={ optionValue === 'read'}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</>
</FormControl>
);
}}
/>
{/* <Controller
name="isReadEnabled"
defaultValue={true}
control={control}
render={({ field: { onChange, ... field }, fieldState }) => {
return (
<Checkbox
className="data-[state=checked]:bg-primary"
isChecked={field.value}
onCheckedChange={(state) => {
onChange(state);
}}
>
Read (default)
</Checkbox>
);
}}
/>
<Controller
name="isWriteEnabled"
defaultValue={false}
control={control}
render={({ field: { onChange, ... field }, fieldState }) => {
return (
<Checkbox
className="data-[state=checked]:bg-primary"
isChecked={field.value}
onCheckedChange={(state) => {
onChange(state);
}}
>
Write (optional)
</Checkbox>
);
}}
/> */}
<div className="mt-8 flex items-center">
<Button
className="mr-4"

View File

@@ -25,6 +25,6 @@
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "./src/**/*.ts", "./src/**/*.tsx", "./.eslintrc.js"],
"include": ["next-i18next.config.js", "next-env.d.ts", "./src/**/*.ts", "./src/**/*.tsx", "./.eslintrc.js"],
"exclude": ["node_modules"]
}