diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index 0fecc9d2e..9a1a91672 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -8,7 +8,7 @@ import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { PermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas"; +import { ProjectPermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { @@ -39,7 +39,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }) .optional() .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) + permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) }), response: { 200: z.object({ @@ -90,7 +90,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }) .optional() .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), + permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), temporaryMode: z .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), @@ -155,7 +155,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F message: "Slug must be a valid slug" }) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), - permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 483f8e54c..f9a5ef312 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -89,6 +89,9 @@ export const UNIVERSAL_AUTH = { }, RENEW_ACCESS_TOKEN: { accessToken: "The access token to renew." + }, + REVOKE_ACCESS_TOKEN: { + accessToken: "The access token to revoke." } } as const; diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 14155ecf9..cf9f23851 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -8,6 +8,7 @@ import { UsersSchema } from "@app/db/schemas"; import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; // sometimes the return data must be santizied to avoid leaking important values // always prefer pick over omit in zod @@ -64,14 +65,12 @@ export const secretRawSchema = z.object({ secretComment: z.string().optional() }); -export const PermissionSchema = z.object({ +export const ProjectPermissionSchema = z.object({ action: z - .string() - .min(1) + .nativeEnum(ProjectPermissionActions) .describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read"), subject: z - .string() - .min(1) + .nativeEnum(ProjectPermissionSub) .describe("The entity this permission pertains to. Possible options: secrets, environments"), conditions: z .object({ diff --git a/backend/src/server/routes/v1/identity-access-token-router.ts b/backend/src/server/routes/v1/identity-access-token-router.ts index 387c54c13..7ed62e679 100644 --- a/backend/src/server/routes/v1/identity-access-token-router.ts +++ b/backend/src/server/routes/v1/identity-access-token-router.ts @@ -36,4 +36,29 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid }; } }); + + server.route({ + url: "/token/revoke", + method: "POST", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Revoke access token", + body: z.object({ + accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.identityAccessToken.revokeAccessToken(req.body.accessToken); + return { + message: "Successfully revoked access token" + }; + } + }); }; diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 42fb5bba5..de8eb7ebc 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TIdentityAccessTokens } from "@app/db/schemas"; +import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; @@ -15,23 +15,46 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { const doc = await (tx || db)(TableName.IdentityAccessToken) .where(filter) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) - .leftJoin( - TableName.IdentityUaClientSecret, - `${TableName.IdentityAccessToken}.identityUAClientSecretId`, - `${TableName.IdentityUaClientSecret}.id` - ) - .leftJoin( - TableName.IdentityUniversalAuth, - `${TableName.IdentityUaClientSecret}.identityUAId`, - `${TableName.IdentityUniversalAuth}.id` - ) + .leftJoin(TableName.IdentityUaClientSecret, (qb) => { + qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn( + `${TableName.IdentityAccessToken}.identityUAClientSecretId`, + `${TableName.IdentityUaClientSecret}.id` + ); + }) + .leftJoin(TableName.IdentityUniversalAuth, (qb) => { + qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn( + `${TableName.IdentityUaClientSecret}.identityUAId`, + `${TableName.IdentityUniversalAuth}.id` + ); + }) + .leftJoin(TableName.IdentityGcpAuth, (qb) => { + qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.GCP_AUTH])).andOn( + `${TableName.Identity}.id`, + `${TableName.IdentityGcpAuth}.identityId` + ); + }) + .leftJoin(TableName.IdentityAwsAuth, (qb) => { + qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AWS_AUTH])).andOn( + `${TableName.Identity}.id`, + `${TableName.IdentityAwsAuth}.identityId` + ); + }) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select( - db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"), db.ref("name").withSchema(TableName.Identity) ) .first(); - return doc; + + if (!doc) return; + + return { + ...doc, + accessTokenTrustedIps: + doc.accessTokenTrustedIpsUa || doc.accessTokenTrustedIpsGcp || doc.accessTokenTrustedIpsAws + }; } catch (error) { throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); } diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 4b53c8174..898d0bc62 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -106,6 +106,24 @@ export const identityAccessTokenServiceFactory = ({ return { accessToken, identityAccessToken: updatedIdentityAccessToken }; }; + const revokeAccessToken = async (accessToken: string) => { + const appCfg = getConfig(); + + const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & { + identityAccessTokenId: string; + }; + if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw new UnauthorizedError(); + + const identityAccessToken = await identityAccessTokenDAL.findOne({ + [`${TableName.IdentityAccessToken}.id` as "id"]: decodedToken.identityAccessTokenId, + isAccessTokenRevoked: false + }); + if (!identityAccessToken) throw new UnauthorizedError(); + + const revokedToken = await identityAccessTokenDAL.deleteById(identityAccessToken.id); + return { revokedToken }; + }; + const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, @@ -132,5 +150,5 @@ export const identityAccessTokenServiceFactory = ({ return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; }; - return { renewAccessToken, fnValidateIdentityAccessToken }; + return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; }; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 963db1e1e..5f47580e4 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -462,27 +462,39 @@ const syncSecretsAWSParameterStore = async ({ ssm.config.update(config); const metadata = z.record(z.any()).parse(integration.metadata || {}); + const awsParameterStoreSecretsObj: Record = {}; - const params = { - Path: integration.path as string, - Recursive: false, - WithDecryption: true - }; + // now fetch all aws parameter store secrets + let hasNext = true; + let nextToken: string | undefined; + while (hasNext) { + const parameters = await ssm + .getParametersByPath({ + Path: integration.path as string, + Recursive: false, + WithDecryption: true, + MaxResults: 10, + NextToken: nextToken + }) + .promise(); - const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters; + if (parameters.Parameters) { + parameters.Parameters.forEach((parameter) => { + if (parameter.Name) { + const secKey = parameter.Name.substring((integration.path as string).length); + awsParameterStoreSecretsObj[secKey] = parameter; + } + }); + } + hasNext = Boolean(parameters.NextToken); + nextToken = parameters.NextToken; + } - const awsParameterStoreSecretsObj = (parameterList || []) - .filter(({ Name }) => Boolean(Name)) - .reduce( - (obj, secret) => ({ - ...obj, - [(secret.Name as string).substring((integration.path as string).length)]: secret - }), - {} as Record - ); // Identify secrets to create - await Promise.all( - Object.keys(secrets).map(async (key) => { + // don't use Promise.all() and promise map here + // it will cause rate limit + for (const key in secrets) { + if (Object.hasOwn(secrets, key)) { if (!(key in awsParameterStoreSecretsObj)) { // case: secret does not exist in AWS parameter store // -> create secret @@ -517,13 +529,16 @@ const syncSecretsAWSParameterStore = async ({ }) .promise(); } - }) - ); + + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + } + } if (!metadata.shouldDisableDelete) { - // Identify secrets to delete - await Promise.all( - Object.keys(awsParameterStoreSecretsObj).map(async (key) => { + for (const key in awsParameterStoreSecretsObj) { + if (Object.hasOwn(awsParameterStoreSecretsObj, key)) { if (!(key in secrets)) { // case: // -> delete secret @@ -533,8 +548,11 @@ const syncSecretsAWSParameterStore = async ({ }) .promise(); } - }) - ); + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + } + } } }; diff --git a/docs/api-reference/endpoints/universal-auth/revoke-access-token.mdx b/docs/api-reference/endpoints/universal-auth/revoke-access-token.mdx new file mode 100644 index 000000000..082a76544 --- /dev/null +++ b/docs/api-reference/endpoints/universal-auth/revoke-access-token.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke Access Token" +openapi: "POST /api/v1/auth/token/revoke" +--- diff --git a/docs/cli/commands/export.mdx b/docs/cli/commands/export.mdx index 8f6667a5d..16c226084 100644 --- a/docs/cli/commands/export.mdx +++ b/docs/cli/commands/export.mdx @@ -128,6 +128,12 @@ infisical export --template= + + By default imported secrets are available, you can disable it by setting this option to false. + + Default value: `true` + + Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv`, `json` and `yaml` diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index 9cd8f0a80..74aa84947 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -126,6 +126,12 @@ $ infisical run -- npm run dev + + By default imported secrets are available, you can disable it by setting this option to false. + + Default value: `true` + + {" "} diff --git a/docs/cli/faq.mdx b/docs/cli/faq.mdx index cf95457c9..47e89a48f 100644 --- a/docs/cli/faq.mdx +++ b/docs/cli/faq.mdx @@ -13,6 +13,7 @@ If none of the available stores work for you, you can try using the `file` store If you are still experiencing trouble, please seek support. [Learn more about vault command](./commands/vault) + diff --git a/docs/documentation/platform/identities/gcp-auth.mdx b/docs/documentation/platform/identities/gcp-auth.mdx index 42ae819d3..3dc341a9e 100644 --- a/docs/documentation/platform/identities/gcp-auth.mdx +++ b/docs/documentation/platform/identities/gcp-auth.mdx @@ -123,7 +123,7 @@ access the Infisical API using the GCP ID Token authentication method. ```bash curl curl -H "Metadata-Flavor: Google" \ - 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=' + 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=&format=full' ``` diff --git a/docs/mint.json b/docs/mint.json index 65cc6a4a9..e35d05b44 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -417,7 +417,8 @@ "api-reference/endpoints/universal-auth/create-client-secret", "api-reference/endpoints/universal-auth/list-client-secrets", "api-reference/endpoints/universal-auth/revoke-client-secret", - "api-reference/endpoints/universal-auth/renew-access-token" + "api-reference/endpoints/universal-auth/renew-access-token", + "api-reference/endpoints/universal-auth/revoke-access-token" ] }, {