Complete v1 API Key

This commit is contained in:
Tuan Dang
2023-01-15 14:25:23 +07:00
parent 32928bf45c
commit 74b76eda7e
17 changed files with 250 additions and 183 deletions
-5
View File
@@ -335,13 +335,8 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Grraahaam"><img src="https://avatars.githubusercontent.com/u/72856427?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Gabriellopes232"><img src="https://avatars.githubusercontent.com/u/74881862?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/cerrussell"><img src="https://avatars.githubusercontent.com/u/80227828?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/imakecodes"><img src="https://avatars.githubusercontent.com/u/35536648?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/JoaoVictor6"><img src="https://avatars.githubusercontent.com/u/68869379?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mocherfaoui"><img src="https://avatars.githubusercontent.com/u/37941426?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Grraahaam"><img src="https://avatars.githubusercontent.com/u/72856427?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Gabriellopes232"><img src="https://avatars.githubusercontent.com/u/74881862?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/cerrussell"><img src="https://avatars.githubusercontent.com/u/80227828?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/imakecodes"><img src="https://avatars.githubusercontent.com/u/35536648?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
## 🌎 Translations ## 🌎 Translations
<<<<<<< HEAD
Infisical is currently aviable in English and Korean. Help us translate Infisical to your language!
=======
Infisical is currently available in English and Korean. Help us translate Infisical to your language! Infisical is currently available in English and Korean. Help us translate Infisical to your language!
>>>>>>> 9ce4a52b8da0057c2450cd7af93a8c5758c2476b
You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181). You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181).
+1 -1
View File
@@ -111,7 +111,7 @@ app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
app.use('/api/v2/secrets', v2SecretsRouter); app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key-data', v2APIKeyDataRouter); app.use('/api/v2/api-key', v2APIKeyDataRouter);
// api docs // api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
+2 -2
View File
@@ -12,7 +12,7 @@ import { ADMIN, MEMBER } from '../../../variables';
router.get( router.get(
'/:secretId/secret-versions', '/:secretId/secret-versions',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -27,7 +27,7 @@ router.get(
router.post( router.post(
'/:secretId/secret-versions/rollback', '/:secretId/secret-versions/rollback',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
+3 -3
View File
@@ -12,7 +12,7 @@ import { workspaceController } from '../../controllers/v1';
router.get( router.get(
'/:workspaceId/secret-snapshots', '/:workspaceId/secret-snapshots',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -40,7 +40,7 @@ router.get(
router.post( router.post(
'/:workspaceId/secret-snapshots/rollback', '/:workspaceId/secret-snapshots/rollback',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -54,7 +54,7 @@ router.post(
router.get( router.get(
'/:workspaceId/logs', '/:workspaceId/logs',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
+44 -27
View File
@@ -16,49 +16,66 @@ import {
AccountNotFoundError, AccountNotFoundError,
ServiceTokenDataNotFoundError, ServiceTokenDataNotFoundError,
APIKeyDataNotFoundError, APIKeyDataNotFoundError,
UnauthorizedRequestError UnauthorizedRequestError,
BadRequestError
} from '../utils/errors'; } from '../utils/errors';
// TODO 1: check if API key works
// TODO 2: optimize middleware
/** /**
* Validate that auth token value [authTokenValue] falls under one of *
* accepted auth modes [acceptedAuthModes].
* @param {Object} obj * @param {Object} obj
* @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value) * @param {Object} obj.headers - HTTP request headers object
* @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken)
* @returns {String} authMode - auth mode
*/ */
const validateAuthMode = ({ const validateAuthMode = ({
authTokenValue, headers,
acceptedAuthModes acceptedAuthModes
}: { }: {
authTokenValue: string; headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[]; acceptedAuthModes: string[]
}) => { }) => {
let authMode; // TODO: refactor middleware
try { const apiKey = headers['x-api-key'];
switch (authTokenValue.split('.', 1)[0]) { const authHeader = headers['authorization'];
let authTokenType, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
}
if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey';
authTokenValue = apiKey;
}
if (typeof authHeader === 'string') {
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
const [tokenType, tokenValue] = <[string, string]>authHeader.split(' ', 2) ?? [null, null]
if (tokenType === null)
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
if (tokenType.toLowerCase() !== 'bearer')
throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` });
if (tokenValue === null)
throw BadRequestError({ message: 'Missing Authorization Body in the request header.' });
switch (tokenValue.split('.', 1)[0]) {
case 'st': case 'st':
authMode = 'serviceToken'; authTokenType = 'serviceToken';
break;
case 'ak':
authMode = 'apiKey';
break; break;
default: default:
authMode = 'jwt'; authTokenType = 'jwt';
break; }
authTokenValue = tokenValue;
} }
if (!acceptedAuthModes.includes(authMode)) if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
} catch (err) { if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
}
return authMode; return ({
authTokenType,
authTokenValue
});
} }
/** /**
+8 -18
View File
@@ -7,7 +7,6 @@ import {
getAuthSTDPayload, getAuthSTDPayload,
getAuthAPIKeyPayload getAuthAPIKeyPayload
} from '../helpers/auth'; } from '../helpers/auth';
import { BadRequestError } from '../utils/errors';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -31,37 +30,28 @@ const requireAuth = ({
acceptedAuthModes: string[]; acceptedAuthModes: string[];
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] // validate auth token against accepted auth modes [acceptedAuthModes]
if (AUTH_TOKEN_TYPE === null) // and return token type [authTokenType] and value [authTokenValue]
return next(BadRequestError({ message: `Missing Authorization Header in the request header.` })) const { authTokenType, authTokenValue } = validateAuthMode({
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') headers: req.headers,
return next(BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }))
if (AUTH_TOKEN_VALUE === null)
return next(BadRequestError({ message: 'Missing Authorization Body in the request header' }))
// validate auth token against
const authMode = validateAuthMode({
authTokenValue: AUTH_TOKEN_VALUE,
acceptedAuthModes acceptedAuthModes
}); });
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
// attach auth payloads // attach auth payloads
switch (authMode) { switch (authTokenType) {
case 'serviceToken': case 'serviceToken':
req.serviceTokenData = await getAuthSTDPayload({ req.serviceTokenData = await getAuthSTDPayload({
authTokenValue: AUTH_TOKEN_VALUE authTokenValue
}); });
break; break;
case 'apiKey': case 'apiKey':
req.user = await getAuthAPIKeyPayload({ req.user = await getAuthAPIKeyPayload({
authTokenValue: AUTH_TOKEN_VALUE authTokenValue
}); });
break; break;
default: default:
req.user = await getAuthUserPayload({ req.user = await getAuthUserPayload({
authTokenValue: AUTH_TOKEN_VALUE authTokenValue
}); });
break; break;
} }
+4 -4
View File
@@ -61,7 +61,7 @@ router.post(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -76,7 +76,7 @@ router.get(
query('environment').exists().trim(), query('environment').exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken'] acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -115,7 +115,7 @@ router.patch(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -143,7 +143,7 @@ router.delete(
.isEmpty(), .isEmpty(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
+1 -1
View File
@@ -8,7 +8,7 @@ import { usersController } from '../../controllers/v2';
router.get( router.get(
'/me', '/me',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
usersController.getMe usersController.getMe
); );
+4 -4
View File
@@ -45,7 +45,7 @@ router.get(
router.get( router.get(
'/:workspaceId/encrypted-key', '/:workspaceId/encrypted-key',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER]
@@ -75,7 +75,7 @@ router.get( // new - TODO: rewire dashboard to this route
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -89,7 +89,7 @@ router.delete( // TODO - rewire dashboard to this route
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN], acceptedRoles: [ADMIN],
@@ -107,7 +107,7 @@ router.patch( // TODO - rewire dashboard to this route
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]), body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN], acceptedRoles: [ADMIN],
@@ -4,9 +4,10 @@ import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; import addAPIKey from "~/pages/api/apiKey/addAPIKey";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
// import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
// import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { envMapping } from "../../../public/data/frequentConstants"; import { envMapping } from "../../../public/data/frequentConstants";
import { import {
decryptAssymmetric, decryptAssymmetric,
@@ -26,58 +27,33 @@ const expiryMapping = {
const crypto = require('crypto'); const crypto = require('crypto');
// TODO: convert to TS
const AddApiKeyDialog = ({ const AddApiKeyDialog = ({
isOpen, isOpen,
closeModal, closeModal,
workspaceId,
workspaceName, workspaceName,
serviceTokens, apiKeys,
setServiceTokens setApiKeys
}) => { }) => {
const [serviceToken, setServiceToken] = useState(""); const [apiKey, setApiKey] = useState("");
const [serviceTokenName, setServiceTokenName] = useState(""); const [apiKeyName, setApiKeyName] = useState("");
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day");
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [apiKeyCopied, setApiKeyCopied] = useState(false);
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const generateServiceToken = async () => { const generateAPIKey = async () => {
const latestFileKey = await getLatestFileKey({ workspaceId }); const newApiKey = await addAPIKey({
name: apiKeyName,
const key = decryptAssymmetric({ expiresIn: expiryMapping[apiKeyExpiresIn]
ciphertext: latestFileKey.latestKey.encryptedKey,
nonce: latestFileKey.latestKey.nonce,
publicKey: latestFileKey.latestKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY"),
}); });
const randomBytes = crypto.randomBytes(16).toString('hex'); setApiKeys([...apiKeys, newApiKey.apiKeyData])
const { setApiKey(newApiKey.apiKey);
ciphertext,
iv,
tag,
} = encryptSymmetric({
plaintext: key,
key: randomBytes,
});
let newServiceToken = await addServiceToken({
name: serviceTokenName,
workspaceId,
environment: envMapping[serviceTokenEnv],
expiresIn: expiryMapping[serviceTokenExpiresIn],
encryptedKey: ciphertext,
iv,
tag
});
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
}; };
function copyToClipboard() { function copyToClipboard() {
// Get the text field // Get the text field
var copyText = document.getElementById("serviceToken"); var copyText = document.getElementById("apiKey");
// Select the text field // Select the text field
copyText.select(); copyText.select();
@@ -86,16 +62,16 @@ const AddApiKeyDialog = ({
// Copy the text inside the text field // Copy the text inside the text field
navigator.clipboard.writeText(copyText.value); navigator.clipboard.writeText(copyText.value);
setServiceTokenCopied(true); setApiKeyCopied(true);
setTimeout(() => setServiceTokenCopied(false), 2000); setTimeout(() => setApiKeyCopied(false), 2000);
// Alert the copied text // Alert the copied text
// alert("Copied the text: " + copyText.value); // alert("Copied the text: " + copyText.value);
} }
const closeAddServiceTokenModal = () => { const closeAddApiKeyModal = () => {
closeModal(); closeModal();
setServiceTokenName(""); setApiKeyName("");
setServiceToken(""); setApiKey("");
}; };
return ( return (
@@ -125,51 +101,37 @@ const AddApiKeyDialog = ({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
{serviceToken == "" ? ( {apiKey == "" ? (
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" className="text-lg font-medium leading-6 text-gray-400 z-50"
> >
{t("section-token:add-dialog.title", { {t("section-api-key:add-dialog.title", {
target: workspaceName, target: workspaceName,
})} })}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-4"> <div className="mt-2 mb-4">
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{t("section-token:add-dialog.description")} {t("section-api-key:add-dialog.description")}
</p> </p>
</div> </div>
</div> </div>
<div className="max-h-28 mb-2"> <div className="max-h-28 mb-2">
<InputField <InputField
label={t("section-token:add-dialog.name")} label={t("section-api-key:add-dialog.name")}
onChangeHandler={setServiceTokenName} onChangeHandler={setApiKeyName}
type="varName" type="varName"
value={serviceTokenName} value={apiKeyName}
placeholder="" placeholder=""
isRequired isRequired
/> />
</div> </div>
<div className="max-h-28 mb-2">
<ListBox
selected={serviceTokenEnv}
onChange={setServiceTokenEnv}
data={[
"Development",
"Staging",
"Production",
"Testing",
]}
isFull={true}
text={`${t("common:environment")}: `}
/>
</div>
<div className="max-h-28"> <div className="max-h-28">
<ListBox <ListBox
selected={serviceTokenExpiresIn} selected={apiKeyExpiresIn}
onChange={setServiceTokenExpiresIn} onChange={setApiKeyExpiresIn}
data={[ data={[
"1 day", "1 day",
"7 days", "7 days",
@@ -184,12 +146,12 @@ const AddApiKeyDialog = ({
<div className="max-w-max"> <div className="max-w-max">
<div className="mt-6 flex flex-col justify-start w-max"> <div className="mt-6 flex flex-col justify-start w-max">
<Button <Button
onButtonPressed={() => generateServiceToken()} onButtonPressed={() => generateAPIKey()}
color="mineshaft" color="mineshaft"
text={t("section-token:add-dialog.add")} text={t("section-api-key:add-dialog.add")}
textDisabled={t("section-token:add-dialog.add")} textDisabled={t("section-api-key:add-dialog.add")}
size="md" size="md"
active={serviceTokenName == "" ? false : true} active={apiKeyName == "" ? false : true}
/> />
</div> </div>
</div> </div>
@@ -200,13 +162,13 @@ const AddApiKeyDialog = ({
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50" className="text-lg font-medium leading-6 text-gray-400 z-50"
> >
{t("section-token:add-dialog.copy-service-token")} {t("section-api-key:add-dialog.copy-service-token")}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-4"> <div className="mt-2 mb-4">
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{t( {t(
"section-token:add-dialog.copy-service-token-description" "section-api-key:add-dialog.copy-service-token-description"
)} )}
</p> </p>
</div> </div>
@@ -215,19 +177,20 @@ const AddApiKeyDialog = ({
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20"> <div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20">
<input <input
type="text" type="text"
value={serviceToken} value={apiKey}
id="serviceToken" disabled={true}
id="apiKey"
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none" className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
></input> ></input>
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none"> <div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none">
{serviceToken} {apiKey}
</div> </div>
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200"> <div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200">
<button <button
onClick={copyToClipboard} onClick={copyToClipboard}
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200" className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
> >
{serviceTokenCopied ? ( {apiKeyCopied ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheck} icon={faCheck}
className="pr-0.5" className="pr-0.5"
@@ -244,7 +207,7 @@ const AddApiKeyDialog = ({
</div> </div>
<div className="mt-6 flex flex-col justify-start w-max"> <div className="mt-6 flex flex-col justify-start w-max">
<Button <Button
onButtonPressed={() => closeAddServiceTokenModal()} onButtonPressed={() => closeAddApiKeyModal()}
color="mineshaft" color="mineshaft"
text="Close" text="Close"
size="md" size="md"
@@ -2,8 +2,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../../utilities/randomId'; import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
@@ -16,19 +15,18 @@ interface TokenProps {
interface ServiceTokensProps { interface ServiceTokensProps {
data: TokenProps[]; data: TokenProps[];
setServiceTokens: (value: TokenProps[]) => void; setApiKeys: (value: TokenProps[]) => void;
} }
/** /**
* This is the component that we utilize for the api key table * This is the component that we utilize for the api key table
* @param {object} obj * @param {object} obj
* @param {any[]} obj.data - current state of the api key table * @param {any[]} obj.data - current state of the api key table
* @param {function} obj.setServiceTokens - updating the state of the api key table * @param {function} obj.setApiKeys - updating the state of the api key table
* @returns * @returns
*/ */
const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => { const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
return ( return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div> <div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
@@ -36,8 +34,7 @@ const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => {
<thead className="text-bunker-300 text-sm font-light"> <thead className="text-bunker-300 text-sm font-light">
<tr> <tr>
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th> <th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th> <th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -52,9 +49,6 @@ const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => {
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.name} {row.name}
</td> </td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{reverseEnvMapping[row.environment]}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()} {new Date(row.expiresAt).toUTCString()}
</td> </td>
@@ -62,10 +56,10 @@ const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => {
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center"> <div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button <Button
onButtonPressed={() => { onButtonPressed={() => {
deleteServiceToken({ serviceTokenId: row._id} ); deleteAPIKey({ apiKeyId: row._id} );
setServiceTokens(data.filter(token => token._id != row._id)); setApiKeys(data.filter(token => token._id != row._id));
createNotification({ createNotification({
text: `'${row.name}' token has been revoked.`, text: `'${row.name}' API key has been revoked.`,
type: 'error' type: 'error'
}); });
}} }}
+37
View File
@@ -0,0 +1,37 @@
import SecurityClient from '~/utilities/SecurityClient';
interface Props {
name: string;
expiresIn: number;
}
/**
* This route adds an API key for the user
* @param {object} obj
* @param {string} obj.name - name of the API key
* @param {string} obj.expiresIn - how soon the API key expires in ms
* @returns
*/
const addAPIKey = ({
name,
expiresIn,
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
expiresIn
})
}).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to add API key');
}
});
};
export default addAPIKey;
+30
View File
@@ -0,0 +1,30 @@
import SecurityClient from '~/utilities/SecurityClient';
interface Props {
apiKeyId: string;
}
/**
* This route revokes the API key with id [apiKeyId]
* @param {object} obj
* @param {string} obj.apiKeyId - id of the API key to delete
* @returns
*/
const deleteAPIKey = ({
apiKeyId
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/' + apiKeyId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
}).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to delete API key');
}
});
};
export default deleteAPIKey;
+26
View File
@@ -0,0 +1,26 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This route gets API keys for the user
* @param {*} param0
* @returns
*/
const getAPIKeys = () => {
return SecurityClient.fetchCall(
'/api/v2/api-key',
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).apiKeyData;
} else {
console.log('Failed to get API keys');
}
});
};
export default getAPIKeys;
@@ -11,7 +11,7 @@ interface Props {
} }
/** /**
* This route gets service tokens for a specific user in a project * This route adds a service token for a specific user in a project
* @param {object} obj * @param {object} obj
* @param {string} obj.name - name of the service token * @param {string} obj.name - name of the service token
* @param {string} obj.workspaceId - workspace for which we are issuing the token * @param {string} obj.workspaceId - workspace for which we are issuing the token
+18 -16
View File
@@ -16,6 +16,8 @@ import passwordCheck from "~/utilities/checks/PasswordCheck";
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog"; import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
import deleteAPIKey from "../../api/apiKey/deleteAPIKey";
import getAPIKeys from "../../api/apiKey/getAPIKeys";
import getUser from "../../api/user/getUser"; import getUser from "../../api/user/getUser";
export default function PersonalSettings() { export default function PersonalSettings() {
@@ -43,10 +45,21 @@ export default function PersonalSettings() {
localStorage.setItem("lang", to); localStorage.setItem("lang", to);
}; };
useEffect(async () => { useEffect(() => {
const load = async () => {
try {
let user = await getUser(); let user = await getUser();
setApiKeys(
await getAPIKeys()
);
setPersonalEmail(user.email); setPersonalEmail(user.email);
setPersonalName(user.firstName + " " + user.lastName); setPersonalName(user.firstName + " " + user.lastName);
} catch (err) {
console.error(err);
}
}
load();
}, []); }, []);
const closeAddApiKeyModal = () => { const closeAddApiKeyModal = () => {
@@ -63,10 +76,9 @@ export default function PersonalSettings() {
</Head> </Head>
<AddApiKeyDialog <AddApiKeyDialog
isOpen={isAddApiKeyDialogOpen} isOpen={isAddApiKeyDialogOpen}
workspaceId={router.query.id}
closeModal={closeAddApiKeyModal} closeModal={closeAddApiKeyModal}
serviceTokens={apiKeys} apiKeys={apiKeys}
setServiceTokens={setApiKeys} setApiKeys={setApiKeys}
/> />
<div className="flex flex-row"> <div className="flex flex-row">
<div className="w-full max-h-screen pb-2 overflow-y-auto"> <div className="w-full max-h-screen pb-2 overflow-y-auto">
@@ -108,17 +120,6 @@ export default function PersonalSettings() {
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">
{t("settings-personal:api-keys.description")} {t("settings-personal:api-keys.description")}
</p> </p>
<p className="text-sm text-gray-400 mb-4">
Please, make sure you are on the
<a
className="text-primary underline underline-offset-2 ml-1"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noreferrer"
>
latest version of CLI
</a>.
</p>
</div> </div>
<div className="w-48 mt-2"> <div className="w-48 mt-2">
<Button <Button
@@ -134,7 +135,7 @@ export default function PersonalSettings() {
</div> </div>
<ApiKeyTable <ApiKeyTable
data={apiKeys} data={apiKeys}
setServiceTokens={setApiKeys} setApiKeys={setApiKeys}
/> />
</div> </div>
@@ -365,4 +366,5 @@ export const getServerSideProps = getTranslatedServerSideProps([
"settings", "settings",
"settings-personal", "settings-personal",
"section-password", "section-password",
"section-api-key"
]); ]);
@@ -0,0 +1,13 @@
{
"api-keys": "Service Tokens",
"api-keys-description": "Every service token is specific to you, a certain project and a certain environment within this project.",
"add-new": "Add New Token",
"add-dialog": {
"title": "Add an API Key",
"description": "Specify the name and expiry period. When an API key is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere.",
"name": "API Key Name",
"add": "Add API Key",
"copy-service-token": "Copy your API key",
"copy-service-token-description": "Once you close this popup, you will never see your API key again"
}
}