diff --git a/backend/src/db/migrations/20240609133400_private-key-handoff.ts b/backend/src/db/migrations/20240609133400_private-key-handoff.ts new file mode 100644 index 000000000..9741c6029 --- /dev/null +++ b/backend/src/db/migrations/20240609133400_private-key-handoff.ts @@ -0,0 +1,61 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword"); + const doesPrivateKeyFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKey" + ); + const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyIV" + ); + const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyTag" + ); + const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyEncoding" + ); + if (await knex.schema.hasTable(TableName.UserEncryptionKey)) { + await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => { + if (!doesPasswordFieldExist) t.string("hashedPassword"); + if (!doesPrivateKeyFieldExist) t.text("serverEncryptedPrivateKey"); + if (!doesPrivateKeyIVFieldExist) t.text("serverEncryptedPrivateKeyIV"); + if (!doesPrivateKeyTagFieldExist) t.text("serverEncryptedPrivateKeyTag"); + if (!doesPrivateKeyEncodingFieldExist) t.text("serverEncryptedPrivateKeyEncoding"); + }); + } +} + +export async function down(knex: Knex): Promise { + const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword"); + const doesPrivateKeyFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKey" + ); + const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyIV" + ); + const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyTag" + ); + const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn( + TableName.UserEncryptionKey, + "serverEncryptedPrivateKeyEncoding" + ); + if (await knex.schema.hasTable(TableName.UserEncryptionKey)) { + await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => { + if (doesPasswordFieldExist) t.dropColumn("hashedPassword"); + if (doesPrivateKeyFieldExist) t.dropColumn("serverEncryptedPrivateKey"); + if (doesPrivateKeyIVFieldExist) t.dropColumn("serverEncryptedPrivateKeyIV"); + if (doesPrivateKeyTagFieldExist) t.dropColumn("serverEncryptedPrivateKeyTag"); + if (doesPrivateKeyEncodingFieldExist) t.dropColumn("serverEncryptedPrivateKeyEncoding"); + }); + } +} diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts index 693b73b4c..fd9d21a9d 100644 --- a/backend/src/db/schemas/user-encryption-keys.ts +++ b/backend/src/db/schemas/user-encryption-keys.ts @@ -21,7 +21,12 @@ export const UserEncryptionKeysSchema = z.object({ tag: z.string(), salt: z.string(), verifier: z.string(), - userId: z.string().uuid() + userId: z.string().uuid(), + hashedPassword: z.string().nullable().optional(), + serverEncryptedPrivateKey: z.string().nullable().optional(), + serverEncryptedPrivateKeyIV: z.string().nullable().optional(), + serverEncryptedPrivateKeyTag: z.string().nullable().optional(), + serverEncryptedPrivateKeyEncoding: z.string().nullable().optional() }); export type TUserEncryptionKeys = z.infer; diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index e146668c2..8cccbaac6 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -53,7 +53,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { // eslint-disable-next-line async (req: IncomingMessage, user, cb) => { try { - if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." }); + if (!user.mail) throw new BadRequestError({ message: "Invalid request. Missing mail attribute on user." }); const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig; let groups: { dn: string; cn: string }[] | undefined; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 546063132..f5b482c3a 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -73,7 +73,13 @@ type TLdapConfigServiceFactoryDep = { >; userDAL: Pick< TUserDALFactory, - "create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find" + | "create" + | "findOne" + | "transaction" + | "updateById" + | "findUserEncKeyByUserIdsBatch" + | "find" + | "findUserEncKeyByUserId" >; userAliasDAL: Pick; permissionService: Pick; @@ -607,12 +613,14 @@ export const ldapConfigServiceFactory = ({ }); const isUserCompleted = Boolean(user.isAccepted); + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const providerAuthToken = jwt.sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, userId: user.id, username: user.username, + hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey), ...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }), firstName, lastName, diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index c4e6dc2bd..08a13bf06 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -41,7 +41,10 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f type TSamlConfigServiceFactoryDep = { samlConfigDAL: Pick; - userDAL: Pick; + userDAL: Pick< + TUserDALFactory, + "create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId" + >; userAliasDAL: Pick; orgDAL: Pick< TOrgDALFactory, @@ -467,6 +470,7 @@ export const samlConfigServiceFactory = ({ await licenseService.updateSubscriptionOrgMemberCount(organization.id); const isUserCompleted = Boolean(user.isAccepted); + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const providerAuthToken = jwt.sign( { authTokenType: AuthTokenType.PROVIDER_TOKEN, @@ -479,6 +483,7 @@ export const samlConfigServiceFactory = ({ organizationId: organization.id, organizationSlug: organization.slug, authMethod: authProvider, + hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey), authType: UserAliasType.SAML, isUserCompleted, ...(relayState diff --git a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts index 92c6b611d..4092bf356 100644 --- a/backend/src/ee/services/secret-snapshot/snapshot-dal.ts +++ b/backend/src/ee/services/secret-snapshot/snapshot-dal.ts @@ -331,8 +331,8 @@ export const snapshotDALFactory = (db: TDbClient) => { * Prunes excess snapshots from the database to ensure only a specified number of recent snapshots are retained for each folder. * * This function operates in three main steps: - * 1. Pruning snapshots from root/non-versioned folders. - * 2. Pruning snapshots from versioned folders. + * 1. Pruning snapshots from current folders. + * 2. Pruning snapshots from non-current folders (versioned ones). * 3. Removing orphaned snapshots that do not belong to any existing folder or folder version. * * The function processes snapshots in batches, determined by the `PRUNE_FOLDER_BATCH_SIZE` constant, @@ -350,7 +350,7 @@ export const snapshotDALFactory = (db: TDbClient) => { try { let uuidOffset = "00000000-0000-0000-0000-000000000000"; - // cleanup snapshots from root/non-versioned folders + // cleanup snapshots from current folders // eslint-disable-next-line no-constant-condition, no-unreachable-loop while (true) { const folderBatch = await db(TableName.SecretFolder) @@ -382,12 +382,11 @@ export const snapshotDALFactory = (db: TDbClient) => { .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`) .join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`) .join("snapshot_cte", "snapshot_cte.id", `${TableName.Snapshot}.id`) - .whereNull(`${TableName.SecretFolder}.parentId`) .whereRaw(`snapshot_cte.row_num > ${TableName.Project}."pitVersionLimit"`) .delete(); } catch (err) { logger.error( - `Failed to prune snapshots from root/non-versioned folders in range ${batchEntries[0]}:${ + `Failed to prune snapshots from current folders in range ${batchEntries[0]}:${ batchEntries[batchEntries.length - 1] }` ); @@ -399,7 +398,7 @@ export const snapshotDALFactory = (db: TDbClient) => { } } - // cleanup snapshots from versioned folders + // cleanup snapshots from non-current folders uuidOffset = "00000000-0000-0000-0000-000000000000"; // eslint-disable-next-line no-constant-condition while (true) { @@ -440,7 +439,7 @@ export const snapshotDALFactory = (db: TDbClient) => { .delete(); } catch (err) { logger.error( - `Failed to prune snapshots from versioned folders in range ${batchEntries[0]}:${ + `Failed to prune snapshots from non-current folders in range ${batchEntries[0]}:${ batchEntries[batchEntries.length - 1] }` ); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 80a2111fc..f4da71293 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -29,7 +29,7 @@ const envSchema = z DB_USER: zpStr(z.string().describe("Postgres database username").optional()), DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()), DB_NAME: zpStr(z.string().describe("Postgres database name").optional()), - + BCRYPT_SALT_ROUND: z.number().default(12), NODE_ENV: z.enum(["development", "test", "production"]).default("production"), SALT_ROUNDS: z.coerce.number().default(10), INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()), diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index bc29cdb3f..8d7ea656a 100644 --- a/backend/src/lib/crypto/srp.ts +++ b/backend/src/lib/crypto/srp.ts @@ -6,7 +6,7 @@ import tweetnacl from "tweetnacl-util"; import { TUserEncryptionKeys } from "@app/db/schemas"; -import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption"; +import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption"; export const generateSrpServerKey = async (salt: string, verifier: string) => { // eslint-disable-next-line new-cap @@ -97,7 +97,13 @@ export const generateUserSrpKeys = async (email: string, password: string) => { }; }; -export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { +export const getUserPrivateKey = async ( + password: string, + user: Pick< + TUserEncryptionKeys, + "protectedKeyTag" | "protectedKey" | "protectedKeyIV" | "encryptedPrivateKey" | "iv" | "salt" | "tag" + > +) => { const derivedKey = await argon2.hash(password, { salt: Buffer.from(user.salt), memoryCost: 65536, @@ -108,17 +114,18 @@ export const getUserPrivateKey = async (password: string, user: TUserEncryptionK raw: true }); if (!derivedKey) throw new Error("Failed to derive key from password"); - const key = decryptSymmetric({ - ciphertext: user.protectedKey!, - iv: user.protectedKeyIV!, - tag: user.protectedKeyTag!, - key: derivedKey.toString("base64") + const key = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: user.protectedKey as string, + iv: user.protectedKeyIV as string, + tag: user.protectedKeyTag as string, + key: derivedKey }); - const privateKey = decryptSymmetric({ + + const privateKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: user.encryptedPrivateKey, iv: user.iv, tag: user.tag, - key + key: Buffer.from(key, "hex") }); return privateKey; }; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 572409d9b..97c3449d9 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -79,6 +79,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ email: z.string().email().trim(), + password: z.string().trim(), firstName: z.string().trim(), lastName: z.string().trim().optional(), protectedKey: z.string().trim(), diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index d20ea0edc..227345916 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -198,7 +198,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }), response: { 200: z.object({ - identityKubernetesAuth: IdentityKubernetesAuthsSchema + identityKubernetesAuth: IdentityKubernetesAuthResponseSchema }) } }, diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index a8ef3fb77..c94e5d4cb 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -51,7 +51,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyIV: z.string().trim(), encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), - verifier: z.string().trim() + verifier: z.string().trim(), + password: z.string().trim() }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index 60bbec7db..3d8d02e6f 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -259,4 +259,50 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { ); } }); + + server.route({ + url: "/token-exchange", + method: "POST", + schema: { + body: z.object({ + providerAuthToken: z.string(), + email: z.string() + }) + }, + handler: async (req, res) => { + const userAgent = req.headers["user-agent"]; + if (!userAgent) throw new Error("user agent header is required"); + + const data = await server.services.login.oauth2TokenExchange({ + email: req.body.email, + ip: req.realIp, + userAgent, + providerAuthToken: req.body.providerAuthToken + }); + + if (data.isMfaEnabled) { + return { mfaEnabled: true, token: data.token } as const; // for discriminated union + } + + void res.setCookie("jid", data.token.refresh, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + return { + mfaEnabled: false, + encryptionVersion: data.user.encryptionVersion, + token: data.token.access, + publicKey: data.user.publicKey, + encryptedPrivateKey: data.user.encryptedPrivateKey, + iv: data.user.iv, + tag: data.user.tag, + protectedKey: data.user.protectedKey || null, + protectedKeyIV: data.user.protectedKeyIV || null, + protectedKeyTag: data.user.protectedKeyTag || null + } as const; + } + }); }; diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index 3d9f531b9..b9269d66e 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -19,7 +19,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true })) + user: UsersSchema.merge( + UserEncryptionKeysSchema.pick({ + clientPublicKey: true, + serverPrivateKey: true, + encryptionVersion: true, + protectedKey: true, + protectedKeyIV: true, + protectedKeyTag: true, + publicKey: true, + encryptedPrivateKey: true, + iv: true, + tag: true, + salt: true, + verifier: true, + userId: true + }) + ) }) } }, @@ -30,6 +46,26 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/private-key", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + privateKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), + handler: async (req) => { + const privateKey = await server.services.user.getUserPrivateKey(req.permission.id); + return { privateKey }; + } + }); + server.route({ method: "GET", url: "/:userId/unlock", diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 1f15008c7..21dd32021 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -255,7 +255,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { description: "Retrieve the current user on the request", response: { 200: z.object({ - user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true })) + user: UsersSchema.merge( + UserEncryptionKeysSchema.pick({ + clientPublicKey: true, + serverPrivateKey: true, + encryptionVersion: true, + protectedKey: true, + protectedKeyIV: true, + protectedKeyTag: true, + publicKey: true, + encryptedPrivateKey: true, + iv: true, + tag: true, + salt: true, + verifier: true, + userId: true + }) + ) }) } }, diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 4c7df5612..61a0c74e5 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { email: z.string().trim(), providerAuthToken: z.string().trim().optional(), clientProof: z.string().trim(), - captchaToken: z.string().trim().optional() + captchaToken: z.string().trim().optional(), + password: z.string().optional() }), response: { 200: z.discriminatedUnion("mfaEnabled", [ @@ -112,7 +113,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { ip: req.realIp, userAgent, providerAuthToken: req.body.providerAuthToken, - clientProof: req.body.clientProof + clientProof: req.body.clientProof, + password: req.body.password }); if (data.isMfaEnabled) { diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index ac43df36d..59131464a 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -102,7 +102,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { verifier: z.string().trim(), organizationName: z.string().trim().min(1), providerAuthToken: z.string().trim().optional().nullish(), - attributionSource: z.string().trim().optional() + attributionSource: z.string().trim().optional(), + password: z.string() }), response: { 200: z.object({ @@ -167,6 +168,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ email: z.string().email().trim(), + password: z.string(), firstName: z.string().trim(), lastName: z.string().trim().optional(), protectedKey: z.string().trim(), diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index ecbf73a48..e8574a8b9 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri if (decodedToken.username !== username) throw new Error("Invalid auth credentials"); if (decodedToken.organizationId) { - return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod }; + return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod, userName: decodedToken.username }; } - return { authMethod: decodedToken.authMethod, orgId: null }; + return { authMethod: decodedToken.authMethod, orgId: null, userName: decodedToken.username }; }; export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => { diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index a136508e7..29a2a176f 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,3 +1,4 @@ +import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { TUsers, UserDeviceSchema } from "@app/db/schemas"; @@ -5,6 +6,8 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; @@ -19,6 +22,7 @@ import { TLoginClientProofDTO, TLoginGenServerPublicKeyDTO, TOauthLoginDTO, + TOauthTokenExchangeDTO, TVerifyMfaTokenDTO } from "./auth-login-type"; import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type"; @@ -101,7 +105,7 @@ export const authLoginServiceFactory = ({ user: TUsers; ip: string; userAgent: string; - organizationId: string | undefined; + organizationId?: string; authMethod: AuthMethod; }) => { const cfg = getConfig(); @@ -178,7 +182,8 @@ export const authLoginServiceFactory = ({ ip, userAgent, providerAuthToken, - captchaToken + captchaToken, + password }: TLoginClientProofDTO) => { const appCfg = getConfig(); @@ -248,14 +253,29 @@ export const authLoginServiceFactory = ({ throw new Error("Failed to authenticate. Try again?"); } - await userDAL.updateUserEncryptionByUserId(userEnc.userId, { - serverPrivateKey: null, - clientPublicKey: null - }); - await userDAL.updateById(userEnc.userId, { consecutiveFailedPasswordAttempts: 0 }); + // from password decrypt the private key + if (password) { + const privateKey = await getUserPrivateKey(password, userEnc); + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey); + await userDAL.updateUserEncryptionByUserId(userEnc.userId, { + serverPrivateKey: null, + clientPublicKey: null, + hashedPassword, + serverEncryptedPrivateKey: ciphertext, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyEncoding: encoding + }); + } else { + await userDAL.updateUserEncryptionByUserId(userEnc.userId, { + serverPrivateKey: null, + clientPublicKey: null + }); + } // send multi factor auth token if they it enabled if (userEnc.isMfaEnabled && userEnc.email) { @@ -499,8 +519,14 @@ export const authLoginServiceFactory = ({ authMethods: [authMethod], isGhost: false }); + } else { + const isLinkingRequired = !user?.authMethods?.includes(authMethod); + if (isLinkingRequired) { + user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] }); + } } - const isLinkingRequired = !user?.authMethods?.includes(authMethod); + + const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const isUserCompleted = user.isAccepted; const providerAuthToken = jwt.sign( { @@ -511,9 +537,9 @@ export const authLoginServiceFactory = ({ isEmailVerified: user.isEmailVerified, firstName: user.firstName, lastName: user.lastName, + hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey), authMethod, isUserCompleted, - isLinkingRequired, ...(callbackPort ? { callbackPort @@ -525,10 +551,71 @@ export const authLoginServiceFactory = ({ expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME } ); - return { isUserCompleted, providerAuthToken }; }; + /** + * Handles OAuth2 token exchange for user login with private key handoff. + * + * The process involves exchanging a provider's authorization token for an Infisical access token. + * The provider token is returned to the client, who then sends it back to obtain the Infisical access token. + * + * This approach is used instead of directly sending the access token for the following reasons: + * 1. To facilitate easier logic changes from SRP OAuth to simple OAuth. + * 2. To avoid attaching the access token to the URL, which could be logged. The provider token has a very short lifespan, reducing security risks. + */ + const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => { + const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); + + const appCfg = getConfig(); + const { authMethod, userName } = decodedProviderToken; + if (!userName) throw new BadRequestError({ message: "Missing user name" }); + const organizationId = + (isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId + ? decodedProviderToken.orgId + : undefined; + + const userEnc = await userDAL.findUserEncKeyByUsername({ + username: email + }); + if (!userEnc) throw new BadRequestError({ message: "Invalid token" }); + if (!userEnc.serverEncryptedPrivateKey) + throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." }); + // send multi factor auth token if they it enabled + if (userEnc.isMfaEnabled && userEnc.email) { + enforceUserLockStatus(Boolean(userEnc.isLocked), userEnc.temporaryLockDateEnd); + + const mfaToken = jwt.sign( + { + authMethod, + authTokenType: AuthTokenType.MFA_TOKEN, + userId: userEnc.userId + }, + appCfg.AUTH_SECRET, + { + expiresIn: appCfg.JWT_MFA_LIFETIME + } + ); + + await sendUserMfaCode({ + userId: userEnc.userId, + email: userEnc.email + }); + + return { isMfaEnabled: true, token: mfaToken } as const; + } + + const token = await generateUserTokens({ + user: { ...userEnc, id: userEnc.userId }, + ip, + userAgent, + authMethod, + organizationId + }); + + return { token, isMfaEnabled: false, user: userEnc } as const; + }; + /* * logout user by incrementing the version by 1 meaning any old session will become invalid * as there number is behind @@ -542,6 +629,7 @@ export const authLoginServiceFactory = ({ loginExchangeClientProof, logout, oauth2Login, + oauth2TokenExchange, resendMfaToken, verifyMfaToken, selectOrganization, diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index 4f73ec996..db57d730e 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -13,6 +13,7 @@ export type TLoginClientProofDTO = { ip: string; userAgent: string; captchaToken?: string; + password?: string; }; export type TVerifyMfaTokenDTO = { @@ -31,3 +32,10 @@ export type TOauthLoginDTO = { authMethod: AuthMethod; callbackPort?: string; }; + +export type TOauthTokenExchangeDTO = { + providerAuthToken: string; + ip: string; + userAgent: string; + email: string; +}; diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index a400c297b..0e6558966 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -1,3 +1,4 @@ +import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; @@ -57,7 +58,8 @@ export const authPaswordServiceFactory = ({ encryptedPrivateKeyTag, salt, verifier, - tokenVersionId + tokenVersionId, + password }: TChangePasswordDTO) => { const userEnc = await userDAL.findUserEncKeyByUserId(userId); if (!userEnc) throw new Error("Failed to find user"); @@ -76,6 +78,8 @@ export const authPaswordServiceFactory = ({ ); if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?"); + const appCfg = getConfig(); + const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND); await userDAL.updateUserEncryptionByUserId(userId, { encryptionVersion: 2, protectedKey, @@ -87,7 +91,8 @@ export const authPaswordServiceFactory = ({ salt, verifier, serverPrivateKey: null, - clientPublicKey: null + clientPublicKey: null, + hashedPassword }); if (tokenVersionId) { diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index cf2aac08d..a52374506 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -10,6 +10,7 @@ export type TChangePasswordDTO = { salt: string; verifier: string; tokenVersionId?: string; + password: string; }; export type TResetPasswordViaBackupKeyDTO = { diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 528cb44fa..8cf2c9d34 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,3 +1,4 @@ +import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { OrgMembershipStatus, TableName } from "@app/db/schemas"; @@ -6,6 +7,8 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; import { isDisposableEmail } from "@app/lib/validator"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; @@ -119,6 +122,7 @@ export const authSignupServiceFactory = ({ const completeEmailAccountSignup = async ({ email, + password, firstName, lastName, providerAuthToken, @@ -137,6 +141,7 @@ export const authSignupServiceFactory = ({ userAgent, authorization }: TCompleteAccountSignupDTO) => { + const appCfg = getConfig(); const user = await userDAL.findOne({ username: email }); if (!user || (user && user.isAccepted)) { throw new Error("Failed to complete account for complete user"); @@ -152,6 +157,17 @@ export const authSignupServiceFactory = ({ validateSignUpAuthorization(authorization, user.id); } + const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND); + const privateKey = await getUserPrivateKey(password, { + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + }); + const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey); const updateduser = await authDAL.transaction(async (tx) => { const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); if (!us) throw new Error("User not found"); @@ -166,7 +182,12 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag + tag: encryptedPrivateKeyTag, + hashedPassword, + serverEncryptedPrivateKeyEncoding: encoding, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKey: ciphertext }, tx ); @@ -227,7 +248,6 @@ export const authSignupServiceFactory = ({ userId: updateduser.info.id }); if (!tokenSession) throw new Error("Failed to create token"); - const appCfg = getConfig(); const accessToken = jwt.sign( { @@ -265,6 +285,7 @@ export const authSignupServiceFactory = ({ ip, salt, email, + password, verifier, firstName, publicKey, @@ -295,6 +316,18 @@ export const authSignupServiceFactory = ({ name: "complete account invite" }); + const appCfg = getConfig(); + const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND); + const privateKey = await getUserPrivateKey(password, { + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + }); + const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey); const updateduser = await authDAL.transaction(async (tx) => { const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); if (!us) throw new Error("User not found"); @@ -310,7 +343,12 @@ export const authSignupServiceFactory = ({ protectedKeyTag, encryptedPrivateKey, iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag + tag: encryptedPrivateKeyTag, + hashedPassword, + serverEncryptedPrivateKeyEncoding: encoding, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKey: ciphertext }, tx ); @@ -343,7 +381,6 @@ export const authSignupServiceFactory = ({ userId: updateduser.info.id }); if (!tokenSession) throw new Error("Failed to create token"); - const appCfg = getConfig(); const accessToken = jwt.sign( { diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts index a37a1cd96..9cd70f8c7 100644 --- a/backend/src/services/auth/auth-signup-type.ts +++ b/backend/src/services/auth/auth-signup-type.ts @@ -1,5 +1,6 @@ export type TCompleteAccountSignupDTO = { email: string; + password: string; firstName: string; lastName?: string; protectedKey: string; @@ -21,6 +22,7 @@ export type TCompleteAccountSignupDTO = { export type TCompleteAccountInviteDTO = { email: string; + password: string; firstName: string; lastName?: string; protectedKey: string; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 8ee8c36bd..f1e1c6be0 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -442,7 +442,34 @@ export const identityKubernetesAuthServiceFactory = ({ const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery); - return { ...updatedKubernetesAuth, orgId: identityMembershipOrg.orgId }; + const updatedCACert = + updatedKubernetesAuth.encryptedCaCert && updatedKubernetesAuth.caCertIV && updatedKubernetesAuth.caCertTag + ? decryptSymmetric({ + ciphertext: updatedKubernetesAuth.encryptedCaCert, + iv: updatedKubernetesAuth.caCertIV, + tag: updatedKubernetesAuth.caCertTag, + key + }) + : ""; + + const updatedTokenReviewerJwt = + updatedKubernetesAuth.encryptedTokenReviewerJwt && + updatedKubernetesAuth.tokenReviewerJwtIV && + updatedKubernetesAuth.tokenReviewerJwtTag + ? decryptSymmetric({ + ciphertext: updatedKubernetesAuth.encryptedTokenReviewerJwt, + iv: updatedKubernetesAuth.tokenReviewerJwtIV, + tag: updatedKubernetesAuth.tokenReviewerJwtTag, + key + }) + : ""; + + return { + ...updatedKubernetesAuth, + orgId: identityMembershipOrg.orgId, + caCert: updatedCACert, + tokenReviewerJwt: updatedTokenReviewerJwt + }; }; const getKubernetesAuth = async ({ diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index bec8f3f37..f1d931b20 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,10 @@ +import bcrypt from "bcrypt"; + import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; +import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; import { TAuthLoginFactory } from "../auth/auth-login-service"; @@ -77,6 +81,7 @@ export const superAdminServiceFactory = ({ firstName, salt, email, + password, verifier, publicKey, protectedKey, @@ -92,6 +97,17 @@ export const superAdminServiceFactory = ({ const existingUser = await userDAL.findOne({ email }); if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" }); + const privateKey = await getUserPrivateKey(password, { + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + }); + const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND); + const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey); const userInfo = await userDAL.transaction(async (tx) => { const newUser = await userDAL.create( { @@ -119,7 +135,12 @@ export const superAdminServiceFactory = ({ iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, verifier, - userId: newUser.id + userId: newUser.id, + hashedPassword, + serverEncryptedPrivateKey: ciphertext, + serverEncryptedPrivateKeyIV: iv, + serverEncryptedPrivateKeyTag: tag, + serverEncryptedPrivateKeyEncoding: encoding }, tx ); diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index e586946f2..e444c8843 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -1,5 +1,6 @@ export type TAdminSignUpDTO = { email: string; + password: string; publicKey: string; salt: string; lastName?: string; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 693078fbd..4ee8bdc1f 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,3 +1,5 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; @@ -230,6 +232,21 @@ export const userServiceFactory = ({ ); }; + const getUserPrivateKey = async (userId: string) => { + const user = await userDAL.findUserEncKeyByUserId(userId); + if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) { + throw new BadRequestError({ message: "Private key not found. Please login again" }); + } + const privateKey = infisicalSymmetricDecrypt({ + ciphertext: user.serverEncryptedPrivateKey, + tag: user.serverEncryptedPrivateKeyTag, + iv: user.serverEncryptedPrivateKeyIV, + keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding + }); + + return privateKey; + }; + return { sendEmailVerificationCode, verifyEmailVerificationCode, @@ -240,6 +257,7 @@ export const userServiceFactory = ({ getMe, createUserAction, getUserAction, - unlockUser + unlockUser, + getUserPrivateKey }; }; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index a56f4c24e..fd1dcc574 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -391,6 +391,7 @@ func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request } func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response response, err := httpClient. R(). @@ -566,3 +567,39 @@ func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDyna return createDynamicSecretLeaseResponse, nil } + +func CallCreateRawSecretsV3(httpClient *resty.Client, request CreateRawSecretV3Request) error { + response, err := httpClient. + R(). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallCreateRawSecretsV3: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return fmt.Errorf("CallCreateRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return nil +} + +func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByNameV3Request) error { + response, err := httpClient. + R(). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallUpdateRawSecretsV3: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return fmt.Errorf("CallUpdateRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 56b9807f7..e09f2275f 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -161,6 +161,14 @@ type Secret struct { PlainTextKey string `json:"plainTextKey"` } +type RawSecret struct { + SecretKey string `json:"secretKey,omitempty"` + SecretValue string `json:"secretValue,omitempty"` + Type string `json:"type,omitempty"` + SecretComment string `json:"secretComment,omitempty"` + ID string `json:"id,omitempty"` +} + type GetEncryptedWorkspaceKeyRequest struct { WorkspaceId string `json:"workspaceId"` } @@ -233,6 +241,7 @@ type GetLoginOneV2Response struct { type GetLoginTwoV2Request struct { Email string `json:"email"` ClientProof string `json:"clientProof"` + Password string `json:"password"` } type GetLoginTwoV2Response struct { @@ -409,12 +418,23 @@ type CreateSecretV3Request struct { SecretPath string `json:"secretPath"` } +type CreateRawSecretV3Request struct { + SecretName string `json:"-"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type,omitempty"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath,omitempty"` + SecretValue string `json:"secretValue"` + SecretComment string `json:"secretComment,omitempty"` + SkipMultilineEncoding bool `json:"skipMultilineEncoding,omitempty"` +} + type DeleteSecretV3Request struct { SecretName string `json:"secretName"` WorkspaceId string `json:"workspaceId"` Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` + Type string `json:"type,omitempty"` + SecretPath string `json:"secretPath,omitempty"` } type UpdateSecretByNameV3Request struct { @@ -427,6 +447,15 @@ type UpdateSecretByNameV3Request struct { SecretValueTag string `json:"secretValueTag"` } +type UpdateRawSecretByNameV3Request struct { + SecretName string `json:"-"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath,omitempty"` + SecretValue string `json:"secretValue"` + Type string `json:"type,omitempty"` +} + type GetSingleSecretByNameV3Request struct { SecretName string `json:"secretName"` WorkspaceId string `json:"workspaceId"` diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index e29895570..f485f76a5 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -900,7 +900,7 @@ var agentCmd = &cobra.Command{ return } - authMethodValid, authStrategy := util.IsAuthMethodValid(agentConfig.Auth.Type) + authMethodValid, authStrategy := util.IsAuthMethodValid(agentConfig.Auth.Type, false) if !authMethodValid { util.PrintErrorMessageAndExit(fmt.Sprintf("The auth method '%s' is not supported.", agentConfig.Auth.Type)) diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 983c19255..c0fea738e 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -55,6 +55,11 @@ var exportCmd = &cobra.Command{ util.HandleError(err) } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + format, err := cmd.Flags().GetString("format") if err != nil { util.HandleError(err) @@ -70,11 +75,6 @@ var exportCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - tagSlugs, err := cmd.Flags().GetString("tags") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -169,9 +169,9 @@ func init() { exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") exportCmd.Flags().Bool("include-imports", true, "Imported linked secrets") - exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + exportCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs") - exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from") + exportCmd.Flags().String("projectId", "", "manually set the projectId to export secrets from") exportCmd.Flags().String("path", "/", "get secrets within a folder path") exportCmd.Flags().String("template", "", "The path to the template file used to render secrets") } diff --git a/cli/packages/cmd/folder.go b/cli/packages/cmd/folder.go index 9cb76a312..538f1e2dc 100644 --- a/cli/packages/cmd/folder.go +++ b/cli/packages/cmd/folder.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "github.com/Infisical/infisical-merge/packages/models" @@ -71,10 +72,6 @@ var getCmd = &cobra.Command{ var createCmd = &cobra.Command{ Use: "create", Short: "Create a folder", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - }, Run: func(cmd *cobra.Command, args []string) { environmentName, _ := cmd.Flags().GetString("env") if !cmd.Flags().Changed("env") { @@ -84,6 +81,16 @@ var createCmd = &cobra.Command{ } } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + folderPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -95,19 +102,31 @@ var createCmd = &cobra.Command{ } if folderName == "" { - util.HandleError(fmt.Errorf("Invalid folder name"), "Folder name cannot be empty") + util.HandleError(errors.New("invalid folder name, folder name cannot be empty")) } - workspaceFile, err := util.GetWorkSpaceFromFile() if err != nil { util.HandleError(err, "Unable to get workspace file") } + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get workspace file") + } + + projectId = workspaceFile.WorkspaceId + } + params := models.CreateFolderParameters{ FolderName: folderName, - WorkspaceId: workspaceFile.WorkspaceId, Environment: environmentName, FolderPath: folderPath, + WorkspaceId: projectId, + } + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + params.InfisicalToken = token.Token } _, err = util.CreateFolder(params) @@ -124,10 +143,6 @@ var createCmd = &cobra.Command{ var deleteCmd = &cobra.Command{ Use: "delete", Short: "Delete a folder", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - }, Run: func(cmd *cobra.Command, args []string) { environmentName, _ := cmd.Flags().GetString("env") @@ -138,6 +153,16 @@ var deleteCmd = &cobra.Command{ } } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + folderPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -149,21 +174,29 @@ var deleteCmd = &cobra.Command{ } if folderName == "" { - util.HandleError(fmt.Errorf("Invalid folder name"), "Folder name cannot be empty") + util.HandleError(errors.New("invalid folder name, folder name cannot be empty")) } - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.HandleError(err, "Unable to get workspace file") + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get workspace file") + } + + projectId = workspaceFile.WorkspaceId } params := models.DeleteFolderParameters{ FolderName: folderName, - WorkspaceId: workspaceFile.WorkspaceId, + WorkspaceId: projectId, Environment: environmentName, FolderPath: folderPath, } + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + params.InfisicalToken = token.Token + } + _, err = util.DeleteFolder(params) if err != nil { util.HandleError(err, "Unable to delete folder") diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 61e24b12f..0efe7af9e 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -34,6 +34,8 @@ import ( "github.com/spf13/cobra" "golang.org/x/crypto/argon2" "golang.org/x/term" + + infisicalSdk "github.com/infisical/go-sdk" ) type params struct { @@ -44,6 +46,86 @@ type params struct { keyLength uint32 } +func handleUniversalAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { + + clientId, err := util.GetCmdFlagOrEnv(cmd, "client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) + + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + clientSecret, err := util.GetCmdFlagOrEnv(cmd, "client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret) +} + +func handleKubernetesAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + serviceAccountTokenPath, err := util.GetCmdFlagOrEnv(cmd, "service-account-token-path", util.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath) +} + +func handleAzureAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return infisicalClient.Auth().AzureAuthLogin(identityId) +} + +func handleGcpIdTokenAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return infisicalClient.Auth().GcpIdTokenAuthLogin(identityId) +} + +func handleGcpIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + serviceAccountKeyFilePath, err := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath) +} + +func handleAwsIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return infisicalClient.Auth().AwsIamAuthLogin(identityId) +} + +func formatAuthMethod(authMethod string) string { + return strings.ReplaceAll(authMethod, "-", " ") +} + const ADD_USER = "Add a new account login" const REPLACE_USER = "Override current logged in user" const EXIT_USER_MENU = "Exit" @@ -56,6 +138,11 @@ var loginCmd = &cobra.Command{ DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { + infisicalClient := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + }) + loginMethod, err := cmd.Flags().GetString("method") if err != nil { util.HandleError(err) @@ -65,12 +152,13 @@ var loginCmd = &cobra.Command{ util.HandleError(err) } - if loginMethod != "user" && loginMethod != "universal-auth" { - util.PrintErrorMessageAndExit("Invalid login method. Please use either 'user' or 'universal-auth'") + authMethodValid, strategy := util.IsAuthMethodValid(loginMethod, true) + if !authMethodValid { + util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", loginMethod)) } + // standalone user auth if loginMethod == "user" { - currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() // if the key can't be found or there is an error getting current credentials from key ring, allow them to override if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) { @@ -133,7 +221,7 @@ var loginCmd = &cobra.Command{ err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored) if err != nil { - log.Error().Msgf("Unable to store your credentials in system vault [%s]") + log.Error().Msgf("Unable to store your credentials in system vault") log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq") log.Debug().Err(err) //return here @@ -160,47 +248,33 @@ var loginCmd = &cobra.Command{ fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage") fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack") Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION)) - } else if loginMethod == "universal-auth" { + } else { - clientId, err := cmd.Flags().GetString("client-id") - if err != nil { - util.HandleError(err) + authStrategies := map[util.AuthStrategyType]func(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error){ + util.AuthStrategy.UNIVERSAL_AUTH: handleUniversalAuthLogin, + util.AuthStrategy.KUBERNETES_AUTH: handleKubernetesAuthLogin, + util.AuthStrategy.AZURE_AUTH: handleAzureAuthLogin, + util.AuthStrategy.GCP_ID_TOKEN_AUTH: handleGcpIdTokenAuthLogin, + util.AuthStrategy.GCP_IAM_AUTH: handleGcpIamAuthLogin, + util.AuthStrategy.AWS_IAM_AUTH: handleAwsIamAuthLogin, } - clientSecret, err := cmd.Flags().GetString("client-secret") - if err != nil { - util.HandleError(err) - } - - if clientId == "" { - clientId = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME) - if clientId == "" { - util.PrintErrorMessageAndExit("Please provide client-id") - } - } - if clientSecret == "" { - clientSecret = os.Getenv(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME) - if clientSecret == "" { - util.PrintErrorMessageAndExit("Please provide client-secret") - } - } - - res, err := util.UniversalAuthLogin(clientId, clientSecret) + credential, err := authStrategies[strategy](cmd, infisicalClient) if err != nil { - util.HandleError(err) + util.HandleError(fmt.Errorf("unable to authenticate with %s [err=%v]", formatAuthMethod(loginMethod), err)) } if plainOutput { - fmt.Println(res.AccessToken) + fmt.Println(credential.AccessToken) return } boldGreen := color.New(color.FgGreen).Add(color.Bold) boldPlain := color.New(color.Bold) time.Sleep(time.Second * 1) - boldGreen.Printf(">>>> Successfully authenticated with Universal Auth!\n\n") - boldPlain.Printf("Universal Auth Access Token:\n%v", res.AccessToken) + boldGreen.Printf(">>>> Successfully authenticated with %s!\n\n", formatAuthMethod(loginMethod)) + boldPlain.Printf("Access Token:\n%v", credential.AccessToken) plainBold := color.New(color.Bold) plainBold.Println("\n\nYou can use this access token to authenticate through other commands in the CLI.") @@ -376,9 +450,12 @@ func init() { rootCmd.AddCommand(loginCmd) loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line") loginCmd.Flags().String("method", "user", "login method [user, universal-auth]") - loginCmd.Flags().String("client-id", "", "client id for universal auth") loginCmd.Flags().Bool("plain", false, "only output the token without any formatting") + loginCmd.Flags().String("client-id", "", "client id for universal auth") loginCmd.Flags().String("client-secret", "", "client secret for universal auth") + loginCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") + loginCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") + loginCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") } func DomainOverridePrompt() (bool, error) { @@ -539,6 +616,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R loginTwoResponseResult, err := api.CallLogin2V2(httpClient, api.GetLoginTwoV2Request{ Email: email, ClientProof: hex.EncodeToString(srpM1), + Password: password, }) if err != nil { diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 04fe2588b..22a4ca65b 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -237,8 +237,8 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { func init() { rootCmd.AddCommand(runCmd) - runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - runCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + runCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("include-imports", true, "Import linked secrets ") diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 423fe1657..43c159c34 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -4,23 +4,17 @@ Copyright (c) 2023 Infisical Inc. package cmd import ( - "crypto/sha256" - "encoding/base64" "fmt" - "os" "regexp" "sort" "strings" - "unicode" "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/crypto" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/visualize" "github.com/go-resty/resty/v2" "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -160,14 +154,19 @@ var secretsSetCmd = &cobra.Command{ } } - secretsPath, err := cmd.Flags().GetString("path") + token, err := util.GetInfisicalToken(cmd) if err != nil { util.HandleError(err, "Unable to parse flag") } - workspaceFile, err := util.GetWorkSpaceFromFile() + projectId, err := cmd.Flags().GetString("projectId") if err != nil { - util.HandleError(err, "Unable to get your local config details") + util.HandleError(err, "Unable to parse flag") + } + + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") } secretType, err := cmd.Flags().GetString("type") @@ -175,196 +174,18 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse secret type") } - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + var secretOperations []models.SecretSetOperation + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token) + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + secretOperations, err = util.SetEncryptedSecrets(args, secretType, environmentName, secretsPath) + } + if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") - } - - - httpClient := resty.New(). - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). - SetHeader("Accept", "application/json") - - request := api.GetEncryptedWorkspaceKeyRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - } - - workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request) - if err != nil { - util.HandleError(err, "unable to get your encrypted workspace key") - } - - encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey) - encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey) - encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce) - currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) - - if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 { - log.Debug().Msgf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey) - util.PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again") - } - - // decrypt workspace key - plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) - - infisicalTokenEnv := os.Getenv(util.INFISICAL_TOKEN_NAME) - - // pull current secrets - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "") - if err != nil { - util.HandleError(err, "unable to retrieve secrets") - } - - type SecretSetOperation struct { - SecretKey string - SecretValue string - SecretOperation string - } - - secretsToCreate := []api.Secret{} - secretsToModify := []api.Secret{} - secretOperations := []SecretSetOperation{} - - sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - - for _, secret := range secrets { - if secret.Type == util.SECRET_TYPE_PERSONAL { - personalSecretMapByName[secret.Key] = secret - } else { - sharedSecretMapByName[secret.Key] = secret - } - } - - for _, arg := range args { - splitKeyValueFromArg := strings.SplitN(arg, "=", 2) - if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { - util.PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again") - } - - if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { - util.PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again") - } - - // Key and value from argument - key := splitKeyValueFromArg[0] - value := splitKeyValueFromArg[1] - - hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) - encryptedKey, err := crypto.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) - if err != nil { - util.HandleError(err, "unable to encrypt your secrets") - } - - hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) - encryptedValue, err := crypto.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) - if err != nil { - util.HandleError(err, "unable to encrypt your secrets") - } - - var existingSecret models.SingleEnvironmentVariable - var doesSecretExist bool - - if secretType == util.SECRET_TYPE_SHARED { - existingSecret, doesSecretExist = sharedSecretMapByName[key] - } else { - existingSecret, doesSecretExist = personalSecretMapByName[key] - } - - if doesSecretExist { - // case: secret exists in project so it needs to be modified - encryptedSecretDetails := api.Secret{ - ID: existingSecret.ID, - SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), - SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), - SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), - SecretValueHash: hashedValue, - PlainTextKey: key, - Type: existingSecret.Type, - } - - // Only add to modifications if the value is different - if existingSecret.Value != value { - secretsToModify = append(secretsToModify, encryptedSecretDetails) - secretOperations = append(secretOperations, SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET VALUE MODIFIED", - }) - } else { - // Current value is same as exisitng so no change - secretOperations = append(secretOperations, SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET VALUE UNCHANGED", - }) - } - - } else { - // case: secret doesn't exist in project so it needs to be created - encryptedSecretDetails := api.Secret{ - SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), - SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), - SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), - SecretKeyHash: hashedKey, - SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), - SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), - SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), - SecretValueHash: hashedValue, - Type: secretType, - PlainTextKey: key, - } - secretsToCreate = append(secretsToCreate, encryptedSecretDetails) - secretOperations = append(secretOperations, SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET CREATED", - }) - } - } - - for _, secret := range secretsToCreate { - createSecretRequest := api.CreateSecretV3Request{ - WorkspaceID: workspaceFile.WorkspaceId, - Environment: environmentName, - SecretName: secret.PlainTextKey, - SecretKeyCiphertext: secret.SecretKeyCiphertext, - SecretKeyIV: secret.SecretKeyIV, - SecretKeyTag: secret.SecretKeyTag, - SecretValueCiphertext: secret.SecretValueCiphertext, - SecretValueIV: secret.SecretValueIV, - SecretValueTag: secret.SecretValueTag, - Type: secret.Type, - SecretPath: secretsPath, - } - - err = api.CallCreateSecretsV3(httpClient, createSecretRequest) - if err != nil { - util.HandleError(err, "Unable to process new secret creations") - return - } - } - - for _, secret := range secretsToModify { - updateSecretRequest := api.UpdateSecretByNameV3Request{ - WorkspaceID: workspaceFile.WorkspaceId, - Environment: environmentName, - SecretValueCiphertext: secret.SecretValueCiphertext, - SecretValueIV: secret.SecretValueIV, - SecretValueTag: secret.SecretValueTag, - Type: secret.Type, - SecretPath: secretsPath, - } - - err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey) - if err != nil { - util.HandleError(err, "Unable to process secret update request") - return - } + util.HandleError(err, "Unable to set secrets") } // Print secret operations @@ -395,6 +216,16 @@ var secretsDeleteCmd = &cobra.Command{ } } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + secretsPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -405,33 +236,44 @@ var secretsDeleteCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() - if err != nil { - util.HandleError(err, "Unable to authenticate") + httpClient := resty.New(). + SetHeader("Accept", "application/json") + + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get local project details") + } + projectId = workspaceFile.WorkspaceId } - if loggedInUserDetails.LoginExpired { - util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") - } + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + httpClient.SetAuthToken(token.Token) + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.HandleError(err, "Unable to get local project details") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken) } for _, secretName := range args { request := api.DeleteSecretV3Request{ - WorkspaceId: workspaceFile.WorkspaceId, + WorkspaceId: projectId, Environment: environmentName, SecretName: secretName, Type: secretType, SecretPath: secretsPath, } - httpClient := resty.New(). - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). - SetHeader("Accept", "application/json") - err = api.CallDeleteSecretsV3(httpClient, request) if err != nil { util.HandleError(err, "Unable to complete your delete request") @@ -789,13 +631,13 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod } func init() { - secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId when using machine identity based auth") secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path") secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) - secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - secretsGetCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + secretsGetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsGetCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") @@ -804,41 +646,37 @@ func init() { secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) + secretsSetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth") secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") - // Only supports logged in users (JWT auth) - secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - } - secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") + secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsDeleteCmd.Flags().String("projectId", "", "manually set the projectId to delete secrets from when using machine identity based auth") secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.AddCommand(secretsDeleteCmd) - // Only supports logged in users (JWT auth) - secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - } - // *** Folders sub command *** folderCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") // Add getCmd, createCmd and deleteCmd flags here getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from") - getCmd.Flags().String("token", "", "Fetch folders using the infisical token") - getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + getCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from when using machine identity based auth") folderCmd.AddCommand(getCmd) // Add createCmd flags here createCmd.Flags().StringP("path", "p", "/", "Path to where the folder should be created") createCmd.Flags().StringP("name", "n", "", "Name of the folder to be created in selected `--path`") + createCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + createCmd.Flags().String("projectId", "", "manually set the project ID for creating folders in when using machine identity based auth") folderCmd.AddCommand(createCmd) // Add deleteCmd flags here deleteCmd.Flags().StringP("path", "p", "/", "Path to the folder to be deleted") + deleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + deleteCmd.Flags().String("projectId", "", "manually set the projectId to delete folders when using machine identity based auth") deleteCmd.Flags().StringP("name", "n", "", "Name of the folder to be deleted within selected `--path`") folderCmd.AddCommand(deleteCmd) @@ -846,8 +684,8 @@ func init() { // ** End of folders sub command - secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + secretsCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets when using machine identity based auth") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 68527c469..6f996d667 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -134,3 +134,9 @@ type MachineIdentityCredentials struct { ClientId string ClientSecret string } + +type SecretSetOperation struct { + SecretKey string + SecretValue string + SecretOperation string +} diff --git a/cli/packages/util/auth.go b/cli/packages/util/auth.go index 75ffe9344..d27bbc2c8 100644 --- a/cli/packages/util/auth.go +++ b/cli/packages/util/auth.go @@ -27,9 +27,9 @@ var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{ AuthStrategy.AWS_IAM_AUTH, } -func IsAuthMethodValid(authMethod string) (isValid bool, strategy AuthStrategyType) { +func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, strategy AuthStrategyType) { - if authMethod == "user" { + if authMethod == "user" && allowUserAuth { return true, "" } diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index 18fe5c888..c7f6de630 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -172,19 +172,28 @@ func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlu // CreateFolder creates a folder in Infisical func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) { - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() - if err != nil { - return models.SingleFolder{}, err - } - if loggedInUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + // If no token is provided, we will try to get the token from the current logged in user + if params.InfisicalToken == "" { + RequireLogin() + RequireLocalWorkspaceFile() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + + if err != nil { + return models.SingleFolder{}, err + } + + if loggedInUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken } // set up resty client httpClient := resty.New() httpClient. - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetAuthToken(params.InfisicalToken). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json") @@ -209,19 +218,29 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er } func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, error) { - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() - if err != nil { - return nil, err - } - if loggedInUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + // If no token is provided, we will try to get the token from the current logged in user + if params.InfisicalToken == "" { + RequireLogin() + RequireLocalWorkspaceFile() + + loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + + if err != nil { + return nil, err + } + + if loggedInUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken } // set up resty client httpClient := resty.New() httpClient. - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetAuthToken(params.InfisicalToken). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json") diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index b8eb66027..9ce8c4a1d 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -273,3 +273,17 @@ func GetEnvVarOrFileContent(envName string, filePath string) (string, error) { return fileContent, nil } + +func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) { + value, flagsErr := cmd.Flags().GetString(flag) + if flagsErr != nil { + return "", flagsErr + } + if value == "" { + value = os.Getenv(envName) + } + if value == "" { + return "", fmt.Errorf("please provide %s flag", flag) + } + return value, nil +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 02c2704bd..08e6b563f 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -1,6 +1,7 @@ package util import ( + "crypto/sha256" "encoding/base64" "encoding/json" "errors" @@ -9,6 +10,7 @@ import ( "path" "regexp" "strings" + "unicode" "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/crypto" @@ -806,3 +808,336 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil } + +func SetEncryptedSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string) ([]models.SecretSetOperation, error) { + + workspaceFile, err := GetWorkSpaceFromFile() + if err != nil { + return nil, fmt.Errorf("unable to get your local config details [err=%v]", err) + } + + loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + if err != nil { + return nil, fmt.Errorf("unable to authenticate [err=%v]", err) + } + + if loggedInUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + request := api.GetEncryptedWorkspaceKeyRequest{ + WorkspaceId: workspaceFile.WorkspaceId, + } + + workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request) + if err != nil { + return nil, fmt.Errorf("unable to get your encrypted workspace key [err=%v]", err) + } + + encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey) + encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey) + encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce) + currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) + + if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 { + log.Debug().Msgf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey) + PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again") + } + + // decrypt workspace key + plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) + + infisicalTokenEnv := os.Getenv(INFISICAL_TOKEN_NAME) + + // pull current secrets + secrets, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "") + if err != nil { + return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err) + } + + secretsToCreate := []api.Secret{} + secretsToModify := []api.Secret{} + secretOperations := []models.SecretSetOperation{} + + sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + + for _, secret := range secrets { + if secret.Type == SECRET_TYPE_PERSONAL { + personalSecretMapByName[secret.Key] = secret + } else { + sharedSecretMapByName[secret.Key] = secret + } + } + + for _, arg := range secretArgs { + splitKeyValueFromArg := strings.SplitN(arg, "=", 2) + if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { + PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again") + } + + if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { + PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again") + } + + // Key and value from argument + key := splitKeyValueFromArg[0] + value := splitKeyValueFromArg[1] + + hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) + encryptedKey, err := crypto.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) + if err != nil { + return nil, fmt.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) + encryptedValue, err := crypto.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) + if err != nil { + return nil, fmt.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + var existingSecret models.SingleEnvironmentVariable + var doesSecretExist bool + + if secretType == SECRET_TYPE_SHARED { + existingSecret, doesSecretExist = sharedSecretMapByName[key] + } else { + existingSecret, doesSecretExist = personalSecretMapByName[key] + } + + if doesSecretExist { + // case: secret exists in project so it needs to be modified + encryptedSecretDetails := api.Secret{ + ID: existingSecret.ID, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + PlainTextKey: key, + Type: existingSecret.Type, + } + + // Only add to modifications if the value is different + if existingSecret.Value != value { + secretsToModify = append(secretsToModify, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE MODIFIED", + }) + } else { + // Current value is same as exisitng so no change + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE UNCHANGED", + }) + } + + } else { + // case: secret doesn't exist in project so it needs to be created + encryptedSecretDetails := api.Secret{ + SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), + SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), + SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), + SecretKeyHash: hashedKey, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + Type: secretType, + PlainTextKey: key, + } + secretsToCreate = append(secretsToCreate, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET CREATED", + }) + } + } + + for _, secret := range secretsToCreate { + createSecretRequest := api.CreateSecretV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secret.PlainTextKey, + SecretKeyCiphertext: secret.SecretKeyCiphertext, + SecretKeyIV: secret.SecretKeyIV, + SecretKeyTag: secret.SecretKeyTag, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, + } + + err = api.CallCreateSecretsV3(httpClient, createSecretRequest) + if err != nil { + return nil, fmt.Errorf("unable to process new secret creations [err=%v]", err) + } + } + + for _, secret := range secretsToModify { + updateSecretRequest := api.UpdateSecretByNameV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, + } + + err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey) + if err != nil { + return nil, fmt.Errorf("unable to process secret update request [err=%v]", err) + } + } + + return secretOperations, nil + +} + +func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) { + + if tokenDetails == nil { + return nil, fmt.Errorf("unable to process set secret operations, token details are missing") + } + + getAllEnvironmentVariablesRequest := models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, WorkspaceId: projectId} + if tokenDetails.Type == UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + getAllEnvironmentVariablesRequest.UniversalAuthAccessToken = tokenDetails.Token + } else { + getAllEnvironmentVariablesRequest.InfisicalToken = tokenDetails.Token + } + + httpClient := resty.New(). + SetAuthToken(tokenDetails.Token). + SetHeader("Accept", "application/json") + + // pull current secrets + secrets, err := GetAllEnvironmentVariables(getAllEnvironmentVariablesRequest, "") + if err != nil { + return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err) + } + + secretsToCreate := []api.RawSecret{} + secretsToModify := []api.RawSecret{} + secretOperations := []models.SecretSetOperation{} + + sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + + for _, secret := range secrets { + if secret.Type == SECRET_TYPE_PERSONAL { + personalSecretMapByName[secret.Key] = secret + } else { + sharedSecretMapByName[secret.Key] = secret + } + } + + for _, arg := range secretArgs { + splitKeyValueFromArg := strings.SplitN(arg, "=", 2) + if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { + PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again") + } + + if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { + PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again") + } + + // Key and value from argument + key := splitKeyValueFromArg[0] + value := splitKeyValueFromArg[1] + + var existingSecret models.SingleEnvironmentVariable + var doesSecretExist bool + + if secretType == SECRET_TYPE_SHARED { + existingSecret, doesSecretExist = sharedSecretMapByName[key] + } else { + existingSecret, doesSecretExist = personalSecretMapByName[key] + } + + if doesSecretExist { + // case: secret exists in project so it needs to be modified + encryptedSecretDetails := api.RawSecret{ + ID: existingSecret.ID, + SecretValue: value, + SecretKey: key, + Type: existingSecret.Type, + } + + // Only add to modifications if the value is different + if existingSecret.Value != value { + secretsToModify = append(secretsToModify, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE MODIFIED", + }) + } else { + // Current value is same as existing so no change + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE UNCHANGED", + }) + } + + } else { + // case: secret doesn't exist in project so it needs to be created + encryptedSecretDetails := api.RawSecret{ + SecretKey: key, + SecretValue: value, + Type: secretType, + } + secretsToCreate = append(secretsToCreate, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET CREATED", + }) + } + } + + for _, secret := range secretsToCreate { + createSecretRequest := api.CreateRawSecretV3Request{ + SecretName: secret.SecretKey, + SecretValue: secret.SecretValue, + Type: secret.Type, + SecretPath: secretsPath, + WorkspaceID: projectId, + Environment: environmentName, + } + + err = api.CallCreateRawSecretsV3(httpClient, createSecretRequest) + if err != nil { + return nil, fmt.Errorf("unable to process new secret creations [err=%v]", err) + } + } + + for _, secret := range secretsToModify { + updateSecretRequest := api.UpdateRawSecretByNameV3Request{ + SecretName: secret.SecretKey, + SecretValue: secret.SecretValue, + SecretPath: secretsPath, + WorkspaceID: projectId, + Environment: environmentName, + Type: secret.Type, + } + + err = api.CallUpdateRawSecretsV3(httpClient, updateSecretRequest) + if err != nil { + return nil, fmt.Errorf("unable to process secret update request [err=%v]", err) + } + } + + return secretOperations, nil + +} diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 6c2bf6a72..d265b546c 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -19,7 +19,7 @@ The changelog below reflects new product developments and updates on a monthly b - Added [Access Requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests) as part of self-serve secrets management workflows. - Added [Temporary Access Provisioning](https://infisical.com/docs/documentation/platform/access-controls/temporary-access) for roles and additional privileges. -## May 2024 +## March 2024 - Released support for [Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview). - Released the concept of [Additional Privileges](https://infisical.com/docs/documentation/platform/access-controls/additional-privileges) on top of user/machine roles. diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index 2758ced00..d97cb4c2b 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -7,32 +7,38 @@ description: "Login into Infisical from the CLI" infisical login ``` -## Description +### Description The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. To change where the login credentials are stored, visit the [vaults command](./vault). If you have added multiple users, you can switch between the users by using the [user command](./user). + + When you authenticate with **any other method than `user`**, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable. + + Use flag `--plain` along with `--silent` to print only the token in plain text when using a machine identity auth method. + + + ### Flags +The login command supports a number of flags that you can use for different authentication methods. Below is a list of all the flags that can be used with the login command. + + ```bash infisical login --method= # Optional, will default to 'user'. ``` #### Valid values for the `method` flag are: - - `user`: Login using email and password. + - `user`: Login using email and password. (default) - `universal-auth`: Login using a universal auth client ID and client secret. - - - When `method` is set to `universal-auth`, the `client-id` and `client-secret` flags are required. Optionally you can set the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` and `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variables instead of using the flags. - - When you authenticate with universal auth, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable. - - Use flag `--plain` along with `--silent` to print only the token in plain text when using the `universal-auth` method. - - + - `kubernetes`: Login using a Kubernetes native auth. + - `azure`: Login using an Azure native auth. + - `gcp-id-token`: Login using a GCP ID token native auth. + - `gcp-iam`: Login using a GCP IAM. + - `aws-iam`: Login using an AWS IAM native auth. @@ -41,7 +47,7 @@ If you have added multiple users, you can switch between the users by using the ``` #### Description - The client ID of the universal auth client. This is required if the `--method` flag is set to `universal-auth`. + The client ID of the universal auth machine identity. This is required if the `--method` flag is set to `universal-auth`. The `client-id` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` environment variable. @@ -52,13 +58,245 @@ If you have added multiple users, you can switch between the users by using the infisical login --client-secret= # Optional, required if --method=universal-auth. ``` #### Description - The client secret of the universal auth client. This is required if the `--method` flag is set to `universal-auth`. + The client secret of the universal auth machine identity. This is required if the `--method` flag is set to `universal-auth`. The `client-secret` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variable. - + + ```bash + infisical login --machine-identity-id= # Optional, required if --method=kubernetes, azure, gcp-id-token, gcp-iam, or aws-iam. + ``` + + #### Description + The ID of the machine identity. This is required if the `--method` flag is set to `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. + + + The `machine-identity-id` flag can be substituted with the `INFISICAL_MACHINE_IDENTITY_ID` environment variable. + + + + ```bash + infisical login --service-account-token-path= # Optional Will default to '/var/run/secrets/kubernetes.io/serviceaccount/token'. + ``` + + #### Description + The path to the Kubernetes service account token to use for authentication. + This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + The `service-account-token-path` flag can be substituted with the `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH` environment variable. + + + + ```bash + infisical login --service-account-key-file-path= # Optional, but required if --method=gcp-iam. + ``` + + #### Description + The path to your GCP service account key file. This is required if the `--method` flag is set to `gcp-iam`. + + + The `service-account-key-path` flag can be substituted with the `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` environment variable. + + + - \ No newline at end of file +### Authentication Methods + +The Infisical CLI supports multiple authentication methods. Below are the available authentication methods, with their respective flags. + + + + The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. + + + + + Your machine identity client ID. + + + Your machine identity client secret. + + + + + + + To create a universal auth machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/universal-auth). + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + infisical login --method=universal-auth --client-id= --client-secret= + ``` + + + + + The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. + + + + + Your machine identity ID. + + + Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + + + + + To create a Kubernetes machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/kubernetes-auth). + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + # --service-account-token-path is optional, and will default to '/var/run/secrets/kubernetes.io/serviceaccount/token' if not provided. + infisical login --method=kubernetes --machine-identity-id= --service-account-token-path= + ``` + + + + + + The Native Azure method is used to authenticate with Infisical when running in an Azure environment. + + + + + Your machine identity ID. + + + + + + + To create an Azure machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/azure-auth). + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + infisical login --method=azure --machine-identity-id= + ``` + + + + + + The Native GCP ID Token method is used to authenticate with Infisical when running in a GCP environment. + + + + + Your machine identity ID. + + + + + + + To create a GCP machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/gcp-auth). + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + infisical login --method=gcp-id-token --machine-identity-id= + ``` + + + + + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. + + + + + Your machine identity ID. + + + Path to your GCP service account key file _(Must be in JSON format!)_ + + + + + + + To create a GCP machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/gcp-auth). + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + infisical login --method=gcp-iam --machine-identity-id= --service-account-key-file-path= + ``` + + + + + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. + + + + + Your machine identity ID. + + + + + + + To create an AWS machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/aws-auth). + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + infisical login --method=aws-iam --machine-identity-id= + ``` + + + + + +### Machine Identity Authentication Quick Start +In this example we'll be using the `universal-auth` method to login to obtain an Infisical access token, which we will then use to fetch secrets with. + + + + ```bash + export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # silent and plain is important to ensure only the token itself is printed, so we can easily set it as an environment variable. + ``` + + Now that we've set the `INFISICAL_TOKEN` environment variable, we can use the CLI to interact with Infisical. The CLI will automatically check for the presence of the `INFISICAL_TOKEN` environment variable and use it for authentication. + + + Alternatively, if you would rather use the `--token` flag to pass the token directly, you can do so by running the following command: + + ```bash + infisical [command] --token= # The token output from the login command. + ``` + + + + ```bash + infisical secrets --projectId= + The `--recursive`, and `--env` flag is optional and will fetch all secrets in subfolders. The default environment is `dev` if no `--env` flag is provided. + + + + +And that's it! Now you're ready to start using the Infisical CLI to interact with your secrets, with the use of Machine Identities. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 489df0ea1..b79dd8815 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "frontend", "dependencies": { "@casl/ability": "^6.5.0", "@casl/react": "^3.1.0", @@ -2109,9 +2110,9 @@ } }, "node_modules/@babel/register": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.23.7.tgz", - "integrity": "sha512-EjJeB6+kvpk+Y5DAkEAmbOBEFkh9OASx0huoEkqYTFxAZHzOAX2Oh5uwAUuL2rUddqfM0SA+KPXV2TbzoZ2kvQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", + "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", @@ -6199,15 +6200,15 @@ } }, "node_modules/@storybook/builder-manager": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.8.tgz", - "integrity": "sha512-4CZo1RHPlDJA7G+lJoVdi+/3/L1ERxVxtvwuGgk8CxVDt6vFNpoc7fEGryNv3GRzKN1/luNYNU1MTnCUSn0B2g==", + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.19.tgz", + "integrity": "sha512-Dt5OLh97xeWh4h2mk9uG0SbCxBKHPhIiHLHAKEIDzIZBdwUhuyncVNDPHW2NlXM+S7U0/iKs2tw05waqh2lHvg==", "dev": true, "dependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@storybook/core-common": "7.6.8", - "@storybook/manager": "7.6.8", - "@storybook/node-logger": "7.6.8", + "@storybook/core-common": "7.6.19", + "@storybook/manager": "7.6.19", + "@storybook/node-logger": "7.6.19", "@types/ejs": "^3.1.1", "@types/find-cache-dir": "^3.2.1", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", @@ -6226,6 +6227,111 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/builder-manager/node_modules/@storybook/channels": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", + "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.6.19", + "@storybook/core-events": "7.6.19", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-manager/node_modules/@storybook/client-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", + "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-manager/node_modules/@storybook/core-common": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", + "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "dev": true, + "dependencies": { + "@storybook/core-events": "7.6.19", + "@storybook/node-logger": "7.6.19", + "@storybook/types": "7.6.19", + "@types/find-cache-dir": "^3.2.1", + "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.4", + "@types/pretty-hrtime": "^1.0.0", + "chalk": "^4.1.0", + "esbuild": "^0.18.0", + "esbuild-register": "^3.5.0", + "file-system-cache": "2.3.0", + "find-cache-dir": "^3.0.0", + "find-up": "^5.0.0", + "fs-extra": "^11.1.0", + "glob": "^10.0.0", + "handlebars": "^4.7.7", + "lazy-universal-dotenv": "^4.0.0", + "node-fetch": "^2.0.0", + "picomatch": "^2.3.0", + "pkg-dir": "^5.0.0", + "pretty-hrtime": "^1.0.3", + "resolve-from": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-manager/node_modules/@storybook/core-events": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", + "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-manager/node_modules/@storybook/node-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", + "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/builder-manager/node_modules/@storybook/types": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", + "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.19", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@storybook/builder-webpack5": { "version": "7.6.8", "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.6.8.tgz", @@ -6332,23 +6438,23 @@ } }, "node_modules/@storybook/cli": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.8.tgz", - "integrity": "sha512-Is8nkgsbIOu+Jk9Z7x5sgMPgGs9RTVDum3cz9eA4UspPiIBJsf7nGHAWOtc+mCIm6Z3eeNbT1YMOWxz9EuqboA==", + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.19.tgz", + "integrity": "sha512-7OVy7nPgkLfgivv6/dmvoyU6pKl9EzWFk+g9izyQHiM/jS8jOiEyn6akG8Ebj6k5pWslo5lgiXUSW+cEEZUnqQ==", "dev": true, "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@ndelangen/get-tarball": "^3.0.7", - "@storybook/codemod": "7.6.8", - "@storybook/core-common": "7.6.8", - "@storybook/core-events": "7.6.8", - "@storybook/core-server": "7.6.8", - "@storybook/csf-tools": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/telemetry": "7.6.8", - "@storybook/types": "7.6.8", + "@storybook/codemod": "7.6.19", + "@storybook/core-common": "7.6.19", + "@storybook/core-events": "7.6.19", + "@storybook/core-server": "7.6.19", + "@storybook/csf-tools": "7.6.19", + "@storybook/node-logger": "7.6.19", + "@storybook/telemetry": "7.6.19", + "@storybook/types": "7.6.19", "@types/semver": "^7.3.4", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", @@ -6373,7 +6479,6 @@ "puppeteer-core": "^2.1.1", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", - "simple-update-notifier": "^2.0.0", "strip-json-comments": "^3.0.1", "tempy": "^1.0.1", "ts-dedent": "^2.0.0", @@ -6388,6 +6493,132 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/cli/node_modules/@storybook/channels": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", + "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.6.19", + "@storybook/core-events": "7.6.19", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/cli/node_modules/@storybook/client-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", + "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/cli/node_modules/@storybook/core-common": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", + "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "dev": true, + "dependencies": { + "@storybook/core-events": "7.6.19", + "@storybook/node-logger": "7.6.19", + "@storybook/types": "7.6.19", + "@types/find-cache-dir": "^3.2.1", + "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.4", + "@types/pretty-hrtime": "^1.0.0", + "chalk": "^4.1.0", + "esbuild": "^0.18.0", + "esbuild-register": "^3.5.0", + "file-system-cache": "2.3.0", + "find-cache-dir": "^3.0.0", + "find-up": "^5.0.0", + "fs-extra": "^11.1.0", + "glob": "^10.0.0", + "handlebars": "^4.7.7", + "lazy-universal-dotenv": "^4.0.0", + "node-fetch": "^2.0.0", + "picomatch": "^2.3.0", + "pkg-dir": "^5.0.0", + "pretty-hrtime": "^1.0.3", + "resolve-from": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/cli/node_modules/@storybook/core-events": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", + "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/cli/node_modules/@storybook/csf-tools": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", + "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "@storybook/csf": "^0.1.2", + "@storybook/types": "7.6.19", + "fs-extra": "^11.1.0", + "recast": "^0.23.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/cli/node_modules/@storybook/node-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", + "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/cli/node_modules/@storybook/types": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", + "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.19", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@storybook/cli/node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6432,26 +6663,11 @@ "node": ">=10.17.0" } }, - "node_modules/@storybook/cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@storybook/cli/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6459,12 +6675,6 @@ "node": ">=10" } }, - "node_modules/@storybook/cli/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@storybook/client-api": { "version": "7.6.8", "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-7.6.8.tgz", @@ -6493,18 +6703,18 @@ } }, "node_modules/@storybook/codemod": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.8.tgz", - "integrity": "sha512-3Gk+ZsD35DUgqbbRNdX547kzZK/ajIbgwynmR0FuPhZhhZuYI4+2eMNzdmI/Oe9Nov4R16senQuAZjw/Dc5LrA==", + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.19.tgz", + "integrity": "sha512-bmHE0iEEgWZ65dXCmasd+GreChjPiWkXu2FEa0cJmNz/PqY12GsXGls4ke1TkNTj4gdSZnbtJxbclPZZnib2tQ==", "dev": true, "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/types": "7.6.8", + "@storybook/csf-tools": "7.6.19", + "@storybook/node-logger": "7.6.19", + "@storybook/types": "7.6.19", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", "globby": "^11.0.2", @@ -6518,6 +6728,97 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/codemod/node_modules/@storybook/channels": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", + "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.6.19", + "@storybook/core-events": "7.6.19", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/codemod/node_modules/@storybook/client-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", + "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/codemod/node_modules/@storybook/core-events": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", + "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/codemod/node_modules/@storybook/csf-tools": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", + "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "@storybook/csf": "^0.1.2", + "@storybook/types": "7.6.19", + "fs-extra": "^11.1.0", + "recast": "^0.23.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/codemod/node_modules/@storybook/node-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", + "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/codemod/node_modules/@storybook/types": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", + "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.19", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@storybook/components": { "version": "7.6.8", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.6.8.tgz", @@ -6762,26 +7063,26 @@ } }, "node_modules/@storybook/core-server": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.8.tgz", - "integrity": "sha512-/csAFNuAhF11f6D9neYNavmKPFK/ZxTskaktc4iDwBRgBM95kZ6DBFjg9ErRi5Q8Z/i92wk6qORkq4bkN/lI9w==", + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.19.tgz", + "integrity": "sha512-7mKL73Wv5R2bEl0kJ6QJ9bOu5YY53Idu24QgvTnUdNsQazp2yUONBNwHIrNDnNEXm8SfCi4Mc9o0mmNRMIoiRA==", "dev": true, "dependencies": { "@aw-web-design/x-default-browser": "1.4.126", "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-manager": "7.6.8", - "@storybook/channels": "7.6.8", - "@storybook/core-common": "7.6.8", - "@storybook/core-events": "7.6.8", + "@storybook/builder-manager": "7.6.19", + "@storybook/channels": "7.6.19", + "@storybook/core-common": "7.6.19", + "@storybook/core-events": "7.6.19", "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.8", + "@storybook/csf-tools": "7.6.19", "@storybook/docs-mdx": "^0.1.0", "@storybook/global": "^5.0.0", - "@storybook/manager": "7.6.8", - "@storybook/node-logger": "7.6.8", - "@storybook/preview-api": "7.6.8", - "@storybook/telemetry": "7.6.8", - "@storybook/types": "7.6.8", + "@storybook/manager": "7.6.19", + "@storybook/node-logger": "7.6.19", + "@storybook/preview-api": "7.6.19", + "@storybook/telemetry": "7.6.19", + "@storybook/types": "7.6.19", "@types/detect-port": "^1.3.0", "@types/node": "^18.0.0", "@types/pretty-hrtime": "^1.0.0", @@ -6794,7 +7095,7 @@ "express": "^4.17.3", "fs-extra": "^11.1.0", "globby": "^11.0.2", - "ip": "^2.0.0", + "ip": "^2.0.1", "lodash": "^4.17.21", "open": "^8.4.0", "pretty-hrtime": "^1.0.3", @@ -6814,26 +7115,163 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/core-server/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@storybook/core-server/node_modules/@storybook/channels": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", + "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "@storybook/client-logger": "7.6.19", + "@storybook/core-events": "7.6.19", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-server/node_modules/@storybook/client-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", + "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-server/node_modules/@storybook/core-common": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", + "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "dev": true, + "dependencies": { + "@storybook/core-events": "7.6.19", + "@storybook/node-logger": "7.6.19", + "@storybook/types": "7.6.19", + "@types/find-cache-dir": "^3.2.1", + "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.4", + "@types/pretty-hrtime": "^1.0.0", + "chalk": "^4.1.0", + "esbuild": "^0.18.0", + "esbuild-register": "^3.5.0", + "file-system-cache": "2.3.0", + "find-cache-dir": "^3.0.0", + "find-up": "^5.0.0", + "fs-extra": "^11.1.0", + "glob": "^10.0.0", + "handlebars": "^4.7.7", + "lazy-universal-dotenv": "^4.0.0", + "node-fetch": "^2.0.0", + "picomatch": "^2.3.0", + "pkg-dir": "^5.0.0", + "pretty-hrtime": "^1.0.3", + "resolve-from": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-server/node_modules/@storybook/core-events": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", + "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-server/node_modules/@storybook/csf-tools": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", + "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "@storybook/csf": "^0.1.2", + "@storybook/types": "7.6.19", + "fs-extra": "^11.1.0", + "recast": "^0.23.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-server/node_modules/@storybook/node-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", + "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-server/node_modules/@storybook/preview-api": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.19.tgz", + "integrity": "sha512-04hdMSQucroJT4dBjQzRd7ZwH2hij8yx2nm5qd4HYGkd1ORkvlH6GOLph4XewNJl5Um3xfzFQzBhvkqvG0WaCQ==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.19", + "@storybook/client-logger": "7.6.19", + "@storybook/core-events": "7.6.19", + "@storybook/csf": "^0.1.2", + "@storybook/global": "^5.0.0", + "@storybook/types": "7.6.19", + "@types/qs": "^6.9.5", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "synchronous-promise": "^2.0.15", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-server/node_modules/@storybook/types": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", + "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.19", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" } }, "node_modules/@storybook/core-server/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6841,12 +7279,6 @@ "node": ">=10" } }, - "node_modules/@storybook/core-server/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@storybook/core-webpack": { "version": "7.6.8", "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.6.8.tgz", @@ -6940,9 +7372,9 @@ "dev": true }, "node_modules/@storybook/manager": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.8.tgz", - "integrity": "sha512-INoXXoHXyw9PPMJAOAhwf9u2GNDDNdv1JAI1fhrbCAECzDabHT9lRVUo6v8I5XMc+YdMHLM1Vz38DbB+w18hFw==", + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.19.tgz", + "integrity": "sha512-fZWQcf59x4P0iiBhrL74PZrqKJAPuk9sWjP8BIkGbf8wTZtUunbY5Sv4225fOL4NLJbuX9/RYLUPoxQ3nucGHA==", "dev": true, "funding": { "type": "opencollective", @@ -7378,14 +7810,14 @@ } }, "node_modules/@storybook/telemetry": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.8.tgz", - "integrity": "sha512-hHUS3fyHjKR3ZdbG+/OVI+pwXXKOmS8L8GMuWKlpUovvCYBLm0/Q0MUQ9XaLuByOCzvAurqB3Owp3ZV7GiY30Q==", + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.19.tgz", + "integrity": "sha512-rA5xum4I36M57iiD3uzmW0MOdpl0vEpHWBSAa5hK0a0ALPeY9TgAsQlI/0dSyNYJ/K7aczEEN6d4qm1NC4u10A==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.8", - "@storybook/core-common": "7.6.8", - "@storybook/csf-tools": "7.6.8", + "@storybook/client-logger": "7.6.19", + "@storybook/core-common": "7.6.19", + "@storybook/csf-tools": "7.6.19", "chalk": "^4.1.0", "detect-package-manager": "^2.0.1", "fetch-retry": "^5.0.2", @@ -7397,6 +7829,132 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@storybook/telemetry/node_modules/@storybook/channels": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", + "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "dev": true, + "dependencies": { + "@storybook/client-logger": "7.6.19", + "@storybook/core-events": "7.6.19", + "@storybook/global": "^5.0.0", + "qs": "^6.10.0", + "telejson": "^7.2.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/telemetry/node_modules/@storybook/client-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", + "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/telemetry/node_modules/@storybook/core-common": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", + "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "dev": true, + "dependencies": { + "@storybook/core-events": "7.6.19", + "@storybook/node-logger": "7.6.19", + "@storybook/types": "7.6.19", + "@types/find-cache-dir": "^3.2.1", + "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.4", + "@types/pretty-hrtime": "^1.0.0", + "chalk": "^4.1.0", + "esbuild": "^0.18.0", + "esbuild-register": "^3.5.0", + "file-system-cache": "2.3.0", + "find-cache-dir": "^3.0.0", + "find-up": "^5.0.0", + "fs-extra": "^11.1.0", + "glob": "^10.0.0", + "handlebars": "^4.7.7", + "lazy-universal-dotenv": "^4.0.0", + "node-fetch": "^2.0.0", + "picomatch": "^2.3.0", + "pkg-dir": "^5.0.0", + "pretty-hrtime": "^1.0.3", + "resolve-from": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/telemetry/node_modules/@storybook/core-events": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", + "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/telemetry/node_modules/@storybook/csf-tools": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", + "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "@storybook/csf": "^0.1.2", + "@storybook/types": "7.6.19", + "fs-extra": "^11.1.0", + "recast": "^0.23.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/telemetry/node_modules/@storybook/node-logger": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", + "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/telemetry/node_modules/@storybook/types": { + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", + "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "dev": true, + "dependencies": { + "@storybook/channels": "7.6.19", + "@types/babel__core": "^7.0.0", + "@types/express": "^4.7.0", + "file-system-cache": "2.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, "node_modules/@storybook/testing-library": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@storybook/testing-library/-/testing-library-0.2.2.tgz", @@ -7897,9 +8455,9 @@ "dev": true }, "node_modules/@types/emscripten": { - "version": "1.39.10", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", - "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", + "version": "1.39.13", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", + "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", "dev": true }, "node_modules/@types/escodegen": { @@ -10066,12 +10624,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -10531,9 +11089,9 @@ } }, "node_modules/citty": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.5.tgz", - "integrity": "sha512-AS7n5NSc0OQVMV9v6wt3ByujNIrne0/cTjiC2MYqhvao57VNfiuVksTSr2p17nVOhEr2KtqiAkGwHcgMC/qUuQ==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "dev": true, "dependencies": { "consola": "^3.2.3" @@ -11862,9 +12420,9 @@ } }, "node_modules/detect-port": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", - "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", "dev": true, "dependencies": { "address": "^1.0.1", @@ -11873,6 +12431,9 @@ "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/detective": { @@ -12306,9 +12867,9 @@ } }, "node_modules/envinfo": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", - "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -13652,9 +14213,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -13828,9 +14389,9 @@ "dev": true }, "node_modules/flow-parser": { - "version": "0.226.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.226.0.tgz", - "integrity": "sha512-YlH+Y/P/5s0S7Vg14RwXlJMF/JsGfkG7gcKB/zljyoqaPNX9YVsGzx+g6MLTbhZaWbPhs4347aTpmSb9GgiPtw==", + "version": "0.237.2", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.237.2.tgz", + "integrity": "sha512-mvI/kdfr3l1waaPbThPA8dJa77nHXrfZIun+SWvFwSwDjmeByU7mGJGRmv1+7guU6ccyLV8e1lqZA1lD4iMGnQ==", "dev": true, "engines": { "node": ">=0.4.0" @@ -14287,18 +14848,18 @@ } }, "node_modules/giget": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", - "integrity": "sha512-4VG22mopWtIeHwogGSy1FViXVo0YT+m6BrqZfz0JJFwbSsePsCdOzdLIIli5BtMp7Xe8f/o2OmBpQX2NBOC24g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", "dev": true, "dependencies": { - "citty": "^0.1.5", + "citty": "^0.1.6", "consola": "^3.2.3", - "defu": "^6.1.3", - "node-fetch-native": "^1.6.1", - "nypm": "^0.3.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", "ohash": "^1.1.3", - "pathe": "^1.1.1", + "pathe": "^1.1.2", "tar": "^6.2.0" }, "bin": { @@ -15856,9 +16417,9 @@ } }, "node_modules/jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", "dev": true, "dependencies": { "async": "^3.2.3", @@ -16024,9 +16585,9 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, "node_modules/jscodeshift": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.1.tgz", - "integrity": "sha512-hIJfxUy8Rt4HkJn/zZPU9ChKfKZM1342waJ1QC2e2YsPcWhM+3BJ4dcfQCzArTrk1jJeNLB341H+qOcEHRxJZg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", + "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", "dev": true, "dependencies": { "@babel/core": "^7.23.0", @@ -17897,9 +18458,9 @@ } }, "node_modules/node-fetch-native": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.1.tgz", - "integrity": "sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", "dev": true }, "node_modules/node-int64": { @@ -18061,15 +18622,16 @@ } }, "node_modules/nypm": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.4.tgz", - "integrity": "sha512-1JLkp/zHBrkS3pZ692IqOaIKSYHmQXgqfELk6YTOfVBnwealAmPA1q2kKK7PHJAHSMBozerThEFZXP3G6o7Ukg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz", + "integrity": "sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==", "dev": true, "dependencies": { - "citty": "^0.1.5", + "citty": "^0.1.6", + "consola": "^3.2.3", "execa": "^8.0.1", - "pathe": "^1.1.1", - "ufo": "^1.3.2" + "pathe": "^1.1.2", + "ufo": "^1.4.0" }, "bin": { "nypm": "dist/cli.mjs" @@ -18147,9 +18709,9 @@ } }, "node_modules/nypm/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -19643,6 +20205,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -19693,6 +20256,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -21653,51 +22217,6 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -21788,9 +22307,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -21804,9 +22323,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "node_modules/split-on-first": { @@ -21902,12 +22421,12 @@ "dev": true }, "node_modules/storybook": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.8.tgz", - "integrity": "sha512-ugRtDSs2eTgHMOZ3wKXbUEbPnlJ2XImPbnvxNssK14py2mHKwPnhSqLNrjlQMkmkO13GdjalLDyj4lZtoYdo0Q==", + "version": "7.6.19", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.19.tgz", + "integrity": "sha512-xWD1C4vD/4KMffCrBBrUpsLUO/9uNpm8BVW8+Vcb30gkQDfficZ0oziWkmLexpT53VSioa24iazGXMwBqllYjQ==", "dev": true, "dependencies": { - "@storybook/cli": "7.6.8" + "@storybook/cli": "7.6.19" }, "bin": { "sb": "index.js", @@ -21997,9 +22516,9 @@ } }, "node_modules/stream-shift": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.2.tgz", - "integrity": "sha512-rV4Bovi9xx0BFzOb/X0B2GqoIjvqPCttZdu0Wgtx2Dxkj7ETyWl9gmqJ4EutWRLvtZWm8dxE+InQZX1IryZn/w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, "node_modules/streamx": { @@ -22548,6 +23067,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -22568,6 +23088,7 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -23161,9 +23682,9 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", "dev": true }, "node_modules/uglify-js": { @@ -24141,9 +24662,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index c98998e35..705b490bb 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -161,6 +161,7 @@ export default function UserInfoStep({ const response = await completeAccountSignup({ email, + password, firstName: name.split(" ")[0], lastName: name.split(" ").slice(1).join(" "), protectedKey, diff --git a/frontend/src/components/utilities/attemptChangePassword.ts b/frontend/src/components/utilities/attemptChangePassword.ts index 5a810334b..45281f2f2 100644 --- a/frontend/src/components/utilities/attemptChangePassword.ts +++ b/frontend/src/components/utilities/attemptChangePassword.ts @@ -72,6 +72,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): }); await changePassword({ + password: newPassword, clientProof, protectedKey, protectedKeyIV, diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index 8f7c4bb9f..6eba08d61 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -71,6 +71,7 @@ const attemptLogin = async ({ tag } = await login2({ email, + password, clientProof, providerAuthToken, captchaToken diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index b909b1ba7..120ae62ec 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -62,6 +62,7 @@ const attemptLogin = async ({ } = await login2({ captchaToken, email, + password, clientProof, providerAuthToken }); diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 6a42e6ed0..8d5f59ede 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -10,6 +10,7 @@ export type TServerConfig = { export type TCreateAdminUserDTO = { email: string; + password: string; firstName: string; lastName?: string; protectedKey: string; diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index 505f7b05f..66688cbdc 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1,5 +1,6 @@ export { useGetAuthToken, + useOauthTokenExchange, useResetPassword, useSelectOrganization, useSendMfaToken, @@ -7,4 +8,5 @@ export { useSendVerificationEmail, useVerifyMfaToken, useVerifyPasswordResetCode, - useVerifySignupEmailVerificationCode} from "./queries"; + useVerifySignupEmailVerificationCode +} from "./queries"; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index cba815fae..fcec4f2ef 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -23,6 +23,7 @@ import { SendMfaTokenDTO, SRP1DTO, SRPR1Res, + TOauthTokenExchangeDTO, VerifyMfaTokenDTO, VerifyMfaTokenRes, VerifySignupInviteDTO @@ -92,6 +93,7 @@ export const useLogin2 = () => { mutationFn: async (details: { email: string; clientProof: string; + password: string; providerAuthToken?: string; }) => { return login2(details); @@ -99,6 +101,20 @@ export const useLogin2 = () => { }); }; +export const oauthTokenExchange = async (details: TOauthTokenExchangeDTO) => { + const { data } = await apiRequest.post("/api/v1/sso/token-exchange", details); + return data; +}; + +export const useOauthTokenExchange = () => { + // note: use after srp1 + return useMutation({ + mutationFn: async (details: TOauthTokenExchangeDTO) => { + return oauthTokenExchange(details); + } + }); +}; + export const srp1 = async (details: SRP1DTO) => { const { data } = await apiRequest.post("/api/v1/password/srp1", details); return data; diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index ce1b18bc8..1b4dff1f0 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -23,6 +23,11 @@ export type VerifyMfaTokenRes = { tag: string; }; +export type TOauthTokenExchangeDTO = { + providerAuthToken: string; + email: string; +}; + export type Login1DTO = { email: string; clientPublicKey: string; @@ -34,6 +39,7 @@ export type Login2DTO = { email: string; clientProof: string; providerAuthToken?: string; + password: string; }; export type Login1Res = { @@ -86,6 +92,7 @@ export type CompleteAccountDTO = { encryptedPrivateKeyTag: string; salt: string; verifier: string; + password: string; }; export type CompleteAccountSignupDTO = CompleteAccountDTO & { @@ -101,6 +108,7 @@ export type VerifySignupInviteDTO = { }; export type ChangePasswordDTO = { + password: string; clientProof: string; protectedKey: string; protectedKeyIV: string; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index a443c6750..fa0b932ea 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -20,6 +20,7 @@ import { export const userKeys = { getUser: ["user"] as const, + getPrivateKey: ["user"] as const, userAction: ["user-action"] as const, getOrgUsers: (orgId: string) => [{ orgId }, "user"], myIp: ["ip"] as const, @@ -351,3 +352,11 @@ export const useGetMyOrganizationProjects = (orgId: string) => { enabled: true }); }; + +export const fetchMyPrivateKey = async () => { + const { + data: { privateKey } + } = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key"); + + return privateKey; +}; diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index a0c2f2430..725dc6113 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -149,6 +149,7 @@ export default function SignupInvite() { const { token: jwtToken } = await completeAccountSignupInvite({ email, + password, firstName, lastName, protectedKey, diff --git a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx index b23af1cef..5e613008d 100644 --- a/frontend/src/views/Login/components/MFAStep/MFAStep.tsx +++ b/frontend/src/views/Login/components/MFAStep/MFAStep.tsx @@ -9,13 +9,12 @@ import Error from "@app/components/basic/Error"; import { createNotification } from "@app/components/notifications"; import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"; import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa"; +import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button } from "@app/components/v2"; -import { useUpdateUserAuthMethods } from "@app/hooks/api"; import { useSendMfaToken } from "@app/hooks/api/auth"; -import { useSelectOrganization } from "@app/hooks/api/auth/queries"; +import { useSelectOrganization, verifyMfaToken } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import { fetchUserDetails } from "@app/hooks/api/users/queries"; -import { AuthMethod } from "@app/hooks/api/users/types"; +import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils"; @@ -56,15 +55,59 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { const { t } = useTranslation(); const sendMfaToken = useSendMfaToken(); - const { mutateAsync: updateUserAuthMethodsMutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync: selectOrganization } = useSelectOrganization(); + // They don't have password + const handleLoginMfaOauth = async (callbackPort: string, organizationId?: string) => { + setIsLoading(true); + const { token } = await verifyMfaToken({ + email, + mfaCode + }); + // + // unset temporary (MFA) JWT token and set JWT token + SecurityClient.setMfaToken(""); + SecurityClient.setToken(token); + SecurityClient.setProviderAuthToken(""); + const privateKey = await fetchMyPrivateKey(); + localStorage.setItem("PRIVATE_KEY", privateKey); + + // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org + if (organizationId) { + const { token: newJwtToken } = await selectOrganization({ organizationId }); + if (callbackPort) { + const cliUrl = `http://127.0.0.1:${callbackPort}/`; + const instance = axios.create(); + await instance.post(cliUrl, { + email, + privateKey, + JTWToken: newJwtToken + }); + } + await navigateUserToOrg(router, organizationId); + } + // case: no organization ID is present -- navigate to the select org page IF the user has any orgs + // if the user has no orgs, navigate to the create org page + else { + const userOrgs = await fetchOrganizations(); + + // case: user has orgs, so we navigate the user to select an org + if (userOrgs.length > 0) { + navigateUserToSelectOrg(router, callbackPort); + } + // case: no orgs found, so we navigate the user to create an org + // cli login will fail in this case + else { + await navigateUserToOrg(router); + } + } + }; + const handleLoginMfa = async () => { try { - let isLinkingRequired: undefined | boolean; let callbackPort: undefined | string; - let authMethod: undefined | AuthMethod; let organizationId: undefined | string; + let hasExchangedPrivateKey: undefined | boolean; const queryParams = new URLSearchParams(window.location.search); @@ -73,10 +116,9 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { if (providerAuthToken) { const decodedToken = jwt_decode(providerAuthToken) as any; - isLinkingRequired = decodedToken.isLinkingRequired; callbackPort = decodedToken.callbackPort; - authMethod = decodedToken.authMethod; organizationId = decodedToken?.organizationId; + hasExchangedPrivateKey = decodedToken?.hasExchangedPrivateKey; } if (mfaCode.length !== 6) { @@ -87,6 +129,11 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { return; } + if (hasExchangedPrivateKey) { + await handleLoginMfaOauth(callbackPort as string, organizationId); + return; + } + setIsLoading(true); if (callbackPort) { // attemptCliLogin @@ -145,14 +192,6 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => { type: "success" }); - if (isLinkingRequired && authMethod) { - const user = await fetchUserDetails(); - const newAuthMethods = [...user.authMethods, authMethod]; - await updateUserAuthMethodsMutateAsync({ - authMethods: newAuthMethods - }); - } - if (organizationId) { await navigateUserToOrg(router, organizationId); } else { diff --git a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx index 2ba90528f..06438a2f8 100644 --- a/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/views/Login/components/PasswordStep/PasswordStep.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef,useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -10,11 +10,11 @@ import { createNotification } from "@app/components/notifications"; import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; import attemptLogin from "@app/components/utilities/attemptLogin"; import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config"; -import { Button, Input } from "@app/components/v2"; -import { useUpdateUserAuthMethods } from "@app/hooks/api"; -import { useSelectOrganization } from "@app/hooks/api/auth/queries"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { Button, Input, Spinner } from "@app/components/v2"; +import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import { fetchUserDetails } from "@app/hooks/api/users/queries"; +import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; import { navigateUserToOrg, navigateUserToSelectOrg } from "../../Login.utils"; @@ -36,12 +36,94 @@ export const PasswordStep = ({ const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const router = useRouter(); - const { mutateAsync } = useUpdateUserAuthMethods(); const { mutateAsync: selectOrganization } = useSelectOrganization(); + const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange(); - const { callbackPort, isLinkingRequired, authMethod, organizationId } = jwt_decode( - providerAuthToken - ) as any; + const { callbackPort, organizationId, hasExchangedPrivateKey } = + jwt_decode(providerAuthToken) as any; + + const handleExchange = async () => { + try { + setIsLoading(true); + const oauthLogin = await oauthTokenExchange({ + email, + providerAuthToken + }); + + // attemptCliLogin + if (oauthLogin.mfaEnabled) { + SecurityClient.setMfaToken(oauthLogin.token); + // case: login requires MFA step + setStep(2); + setIsLoading(false); + return; + } + const cliUrl = `http://127.0.0.1:${callbackPort}/`; + + // case: MFA is not enabled + + // unset provider auth token in case it was used + SecurityClient.setProviderAuthToken(""); + // set JWT token + SecurityClient.setToken(oauthLogin.token); + + const privateKey = await fetchMyPrivateKey(); + localStorage.setItem("PRIVATE_KEY", privateKey); + + // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org + if (organizationId) { + const { token: newJwtToken } = await selectOrganization({ organizationId }); + if (callbackPort) { + console.log("organization id was present. new JWT token to be used in CLI:", newJwtToken); + const instance = axios.create(); + await instance.post(cliUrl, { + privateKey, + email, + JTWToken: newJwtToken + }); + } + + await navigateUserToOrg(router, organizationId); + } + // case: no organization ID is present -- navigate to the select org page IF the user has any orgs + // if the user has no orgs, navigate to the create org page + else { + const userOrgs = await fetchOrganizations(); + + // case: user has orgs, so we navigate the user to select an org + if (userOrgs.length > 0) { + navigateUserToSelectOrg(router, callbackPort); + } + // case: no orgs found, so we navigate the user to create an org + else { + await navigateUserToOrg(router); + } + } + } catch (err: any) { + setIsLoading(false); + console.error(err); + + if (err.response.data.error === "User Locked") { + createNotification({ + title: err.response.data.error, + text: err.response.data.message, + type: "error" + }); + return; + } + + createNotification({ + text: "Login unsuccessful. Double-check your master password and try again.", + type: "error" + }); + } + }; + + useEffect(() => { + if (hasExchangedPrivateKey) { + handleExchange(); + } + }, []); const [captchaToken, setCaptchaToken] = useState(""); const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false); @@ -128,14 +210,6 @@ export const PasswordStep = ({ type: "success" }); - if (isLinkingRequired) { - const user = await fetchUserDetails(); - const newAuthMethods = [...user.authMethods, authMethod]; - await mutateAsync({ - authMethods: newAuthMethods - }); - } - // case: organization ID is present from the provider auth token -- navigate directly to the org if (organizationId) { await navigateUserToOrg(router, organizationId); @@ -183,20 +257,21 @@ export const PasswordStep = ({ setCaptchaToken(""); }; + if (hasExchangedPrivateKey) { + return ( +
+ +

Loading, please wait

+
+ ); + } + return (

- {isLinkingRequired ? "Link your account" : "What's your Infisical password?"} + What's your Infisical password?

- {isLinkingRequired && ( -
- - An existing account without this SSO authentication method enabled was found under the - same email. Login with your password to link the account. - -
- )}
diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 69168e0af..c65317662 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -2,14 +2,10 @@ import crypto from "crypto"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { faInfoCircle, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import jsrp from "jsrp"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; -import InputField from "@app/components/basic/InputField"; -import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; @@ -32,17 +28,6 @@ type Props = { providerAuthToken?: string; }; -type Errors = { - tooShort?: string; - tooLong?: string; - noLetterChar?: string; - noNumOrSpecialChar?: string; - repeatedChar?: string; - escapeChar?: string; - lowEntropy?: string; - breached?: string; -}; - /** * This is the step of the sign up flow where people provife their name/surname and password * @param {object} obj @@ -69,12 +54,13 @@ export const UserInfoSSOStep = ({ const [organizationName, setOrganizationName] = useState(""); const [organizationNameError, setOrganizationNameError] = useState(false); const [attributionSource, setAttributionSource] = useState(""); - const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const { mutateAsync: selectOrganization } = useSelectOrganization(); useEffect(() => { + const randomPassword = crypto.randomBytes(32).toString("hex"); + setPassword(randomPassword); if (providerOrganizationName !== undefined) { setOrganizationName(providerOrganizationName); } @@ -98,11 +84,6 @@ export const UserInfoSSOStep = ({ setOrganizationNameError(false); } - errorCheck = await checkPassword({ - password, - setErrors - }); - if (!errorCheck) { // Generate a random pair of a public and a private key const pair = nacl.box.keyPair(); @@ -158,6 +139,7 @@ export const UserInfoSSOStep = ({ const response = await completeAccountSignup({ email: username, + password, firstName: name.split(" ")[0], lastName: name.split(" ").slice(1).join(" "), protectedKey, @@ -214,6 +196,12 @@ export const UserInfoSSOStep = ({ } }; + useEffect(() => { + if (password && providerOrganizationName) { + signupErrorCheck(); + } + }, [providerOrganizationName, password]); + return (

@@ -272,53 +260,6 @@ export const UserInfoSSOStep = ({ />

)} -
- { - setPassword(pass); - await checkPassword({ - password: pass, - setErrors - }); - }} - type="password" - value={password} - isRequired - error={Object.keys(errors).length > 0} - autoComplete="new-password" - id="new-password" - /> -
- - Infisical Password is used as part of the encryption mechanism so that even the - authentication provider is not able to access your secrets. -
- {Object.keys(errors).length > 0 && ( -
-
- {t("section.password.validate-base")} -
- {Object.keys(errors).map((key) => { - if (errors[key as keyof Errors]) { - return ( -
-
- -
-

{errors[key as keyof Errors]}

-
- ); - } - - return null; - })} -
- )} -