Compare commits

...

97 Commits

Author SHA1 Message Date
Daniel Hougaard
2725e4d9dd Update crypto.ts 2025-07-21 14:29:35 +04:00
Vlad Matsiiako
b413f0f49e Merge pull request #4206 from Infisical/vmatsiiako-patch-readme-careers
Update README.md
2025-07-20 10:34:37 -07:00
Vlad Matsiiako
058dbc144d Update README.md 2025-07-20 10:18:28 -07:00
Maidul Islam
8399181e3d Merge pull request #4204 from Infisical/revert-4114-fix/improveLicenseApiCache
Revert "Removed cloud plan cache as it is handled on the license api"
2025-07-19 10:12:29 -04:00
Maidul Islam
3c50291cd3 Revert "Removed cloud plan cache as it is handled on the license api" 2025-07-19 10:11:05 -04:00
x032205
b7b059bb50 Merge pull request #4117 from Infisical/ENG-3259
feat(app-connection): Gateway support for SQL App Connections + Secret Rotations
2025-07-18 16:59:20 -04:00
Scott Wilson
f3a8e30548 improvement: allow null for non-supported gatewayId 2025-07-18 13:40:42 -07:00
Scott Wilson
b0c93e5c4c Merge pull request #4012 from Infisical/commit-ui-overhaul
improvements(frontend): Revise commit history and commit details UI
2025-07-18 13:24:30 -07:00
x032205
4ab0da6b03 Fix type stuff 2025-07-18 16:22:51 -04:00
carlosmonastyrski
9674b71df8 Merge pull request #4114 from Infisical/fix/improveLicenseApiCache
Removed cloud plan cache as it is handled on the license api
2025-07-18 16:36:32 -03:00
x032205
b7d7b555b2 Only allow gateway for supported connections 2025-07-18 14:42:19 -04:00
Scott Wilson
954ca58e15 chore: revert license 2025-07-18 10:04:57 -07:00
Sid
e4a28ab0f4 fix: render sync fns (#4196)
* fix: improve render-sync fns

* fix: remove double delete
2025-07-18 19:28:03 +05:30
Carlos Monastyrski
4ab8d680c4 Adjust commit diff field highlights to use container width as minimum width 2025-07-18 10:46:21 -03:00
Sid
a3b0d86996 fix: github radar app connection errors and clear aod cookie on signout (#4188)
* fix: clear aod cookie on sign out

* fix: propogate github radar connection errors properly

* chore: add `aod` comment
2025-07-18 13:09:54 +05:30
Scott Wilson
0080d5f291 improvement: remove test css 2025-07-17 19:57:05 -07:00
Scott Wilson
a276d27451 improvement: address feedback 2025-07-17 19:51:14 -07:00
Scott Wilson
cec15d6d51 improvement: address feedback 2025-07-17 17:40:18 -07:00
x032205
007e10d409 Remove console log 2025-07-17 20:29:48 -04:00
x032205
a8b448be0f Swap gateway to outer layer 2025-07-17 20:25:10 -04:00
Sid
bc98c42c79 feat(ENG-3247): add auth origin domain cookie on token creation (#4187)
* feat(ENG-3247): add auth origin domain cookie to multiple routers and update Nginx config
2025-07-18 01:18:25 +05:30
x032205
e6bfb6ce2b Merge branch 'main' into ENG-3259 2025-07-17 15:41:19 -04:00
carlosmonastyrski
1c20e4fef0 Merge pull request #4164 from Infisical/fix/listSecretsThrowOnReadPermissionError
Throw when user does not have permission to read the referenced secret value
2025-07-17 14:51:27 -03:00
Carlos Monastyrski
b560cdb0f8 Improve reference error message 2025-07-17 11:56:27 -03:00
carlosmonastyrski
144143b43a Merge pull request #4184 from Infisical/fix/cliExportFileFlag
Updated CLI export doc to document the new --output-file behavior
2025-07-17 11:12:33 -03:00
carlosmonastyrski
b9a05688cd Merge pull request #4185 from Infisical/fix/pkiImportCertToCaIssueWithDn
On importCertToCa use serialNumber instead of dn to get the parentCa
2025-07-17 10:42:01 -03:00
Carlos Monastyrski
c06c6c6c61 On importCertToCa use serialNumber instead of dn to get the parentCa 2025-07-17 10:28:31 -03:00
Carlos Monastyrski
350afee45e Updated cli export doc 2025-07-17 10:00:40 -03:00
Sid
5ae18a691d fix: verify response type (#4182)
Co-authored-by: sidwebworks <xodeveloper@gmail.com>
2025-07-17 17:59:49 +05:30
Carlos Monastyrski
8187b1da91 Updated CLI export doc to document the new --output-file behavior 2025-07-17 06:58:34 -03:00
Carlos Monastyrski
0174d36136 Throw a meaningful error message when secret reference process fails 2025-07-16 22:41:08 -03:00
Carlos Monastyrski
968d7420c6 Fix typo 2025-07-16 21:58:07 -03:00
Scott Wilson
fd761df8e5 Merge pull request #4178 from Infisical/access-request-env-view
improvement(access-requests): add access requests to single env view + general UI improvements
2025-07-16 16:25:44 -07:00
Scott Wilson
61ca617616 improvement: address feedback 2025-07-16 16:20:10 -07:00
Daniel Hougaard
6ce6c276cd Merge pull request #4180 from Infisical/daniel/tls-auth-docs
docs: document use of port 8433 for TLS certificate auth
2025-07-17 00:45:08 +04:00
Daniel Hougaard
32b2f7b0fe fix typo 2025-07-17 00:20:02 +04:00
Daniel Hougaard
4c2823c480 Update login.mdx 2025-07-17 00:09:56 +04:00
Daniel Hougaard
60438694e4 Update tls-cert-auth.mdx 2025-07-17 00:08:34 +04:00
Maidul Islam
fdaf8f9a87 Merge pull request #4179 from Infisical/doc/added-section-about-sales-approval-design-doc
doc: added section about sales approval
2025-07-16 16:07:36 -04:00
Scott Wilson
3fe41f81fe improvement: address feedback 2025-07-16 12:52:05 -07:00
Sid
c1798d37be fix: propogate Github app connection errors to the client properly (#4177)
* fix: propogate github errors to the client properly
2025-07-17 01:14:06 +05:30
Sheen Capadngan
01c6d3192d doc: added section about sales approval 2025-07-17 03:31:58 +08:00
Scott Wilson
621bfe3e60 chore: revert license 2025-07-16 12:17:43 -07:00
Scott Wilson
67ec00d46b feature: add access requests to single env view, with general UI improvements 2025-07-16 12:16:13 -07:00
x032205
d6c2789d46 Merge pull request #4176 from Infisical/ENG-3154
Make certificate collection required
2025-07-16 14:29:42 -04:00
carlosmonastyrski
58ba0c8ed4 Merge pull request #4175 from Infisical/fix/samlNotVerifiedEmailFix
Add isEmailVerified to isUserCompleted flag on samlLogin
2025-07-16 15:23:52 -03:00
x032205
f38c574030 Address review 2025-07-16 14:01:55 -04:00
x032205
c330d8ca8a Make certificate collection required 2025-07-16 13:53:52 -04:00
Carlos Monastyrski
2cb0ecc768 Add isEmailVerified to isUserCompleted flag on samlLogin 2025-07-16 14:20:37 -03:00
Sid
ecc15bb432 feat(#2938): Add supabase app connection and secrets sync (#4113)
---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Scott Wilson <scottraywilson@gmail.com>
2025-07-16 22:06:11 +05:30
Carlos Monastyrski
59c0f1ff08 List secrets, throw when user does not have permission to read the referenced secret value 2025-07-16 09:14:06 -03:00
Daniel Hougaard
0e07ebae7b fix: oci auth for go sdk (#4152) 2025-07-16 16:36:28 +05:30
x032205
cd84d57025 Lint fixes 2025-07-16 03:19:37 -04:00
x032205
19cb220107 A few tweaks 2025-07-16 03:05:32 -04:00
x032205
fce6738562 Make sql secret rotation use gateway 2025-07-16 02:24:16 -04:00
x032205
aab204a68a feat(app-connection): Gateway support for SQL connections 2025-07-16 01:33:22 -04:00
Scott Wilson
49afaa4d2d improvements: address feedback 2025-07-15 20:03:45 -07:00
carlosmonastyrski
a94a26263a Merge pull request #4115 from Infisical/fix/postgresAppConnectionDocTip
Minor improvement on the Postgres docs changing a warning to a tip
2025-07-15 21:47:42 -03:00
Carlos Monastyrski
b4ef55db4e Minor improvement on the Postgres docs changing a warning to a tip 2025-07-15 21:45:31 -03:00
BlackMagiq
307b5d1f87 Merge pull request #4112 from Infisical/misc/re-added-est
misc: re-added EST to PKI templates
2025-07-15 17:00:24 -07:00
Carlos Monastyrski
2f9baee210 Removed cloud plan cache as it is handled on the license api 2025-07-15 18:21:30 -03:00
Scott Wilson
54087038c2 Merge pull request #4106 from Infisical/secret-change-status-badge
improvement(frontend): add merge/closed status badge to closed secret change request table
2025-07-15 14:03:23 -07:00
carlosmonastyrski
f835bf0ba8 Merge pull request #4111 from Infisical/fix/improvePostgresDocs
Add missing setting for postgres app connection
2025-07-15 16:58:13 -03:00
Sheen Capadngan
c79ea0631e misc: re-added EST 2025-07-16 03:12:49 +08:00
Carlos Monastyrski
948799822f Minor wording improvement 2025-07-15 16:12:16 -03:00
Carlos Monastyrski
c14a431177 Add missing setting for postgres app connection 2025-07-15 16:06:36 -03:00
Sid
7ef077228e feat: Checkly app connection and secrets sync (#4078)
* feat: checkly app connection
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-07-16 00:07:42 +05:30
Scott Wilson
023079be16 Merge pull request #4110 from Infisical/secret-scanning-config-access-banner
improvement(frontend): show upgrade modal and access banner for secret scanning config page when feature is disabled
2025-07-15 11:24:23 -07:00
Scott Wilson
f95bcabef7 improvement: show upgrade modal and access banner for secret scanning config if feature is disabled 2025-07-15 10:57:53 -07:00
Scott Wilson
d5043fdba4 Merge pull request #4109 from Infisical/navbar-org-name-truncation
improvement(frontend): prevent organization name wrap in header
2025-07-15 09:26:43 -07:00
Scott Wilson
3369354904 improvement: prevent organization name wrap in header 2025-07-15 08:54:07 -07:00
Scott Wilson
7ea8c74a0d Merge pull request #4104 from Infisical/update-navbar-styling
improvement(frontend): update styling/consistency for navbar
2025-07-15 08:23:17 -07:00
Daniel Hougaard
bf62aae2e0 Merge pull request #3973 from Infisical/daniel/fips-initative
feat(fips): native FIPS support
2025-07-15 19:03:51 +04:00
Sheen
cd5ca5b34b Merge pull request #4009 from Infisical/misc/update-cli-latest-version-check
misc: update CLI latest version check
2025-07-15 21:17:39 +08:00
Scott Wilson
c4e08b9811 improvement: change closed to rejected and address feedback 2025-07-14 19:15:52 -07:00
Scott Wilson
7784b8a81c improvement: add merge/closed status badge to closed secret change request table 2025-07-14 19:10:28 -07:00
x032205
2f93e2da6c Merge pull request #4105 from Infisical/make-azure-devops-sync-not-require-proj-name 2025-07-14 22:04:35 -04:00
x032205
7f0f5b130a Make Azure DevOps sync not require project name 2025-07-14 21:14:57 -04:00
Scott Wilson
16a084344f improvement: update styling/consistency for navbar 2025-07-14 18:00:52 -07:00
Scott Wilson
374c75521d Merge pull request #4103 from Infisical/allow-users-to-cancel-access-requests
improvement(access-approval): allow all users to reject their own access requests
2025-07-14 16:36:17 -07:00
Scott Wilson
08ccf686ff improvement: allow all users to reject their own access requests 2025-07-14 15:53:48 -07:00
x032205
0c0665dc51 Merge pull request #4011 from Infisical/optimize-token-cleanup-job
Optimize token cleanup job
2025-07-14 18:08:59 -04:00
x032205
2f0a247c11 Describe query 2025-07-14 18:01:35 -04:00
Scott Wilson
bd7947c04e improvements: missed changes 2025-07-14 14:25:33 -07:00
Scott Wilson
0fa6568a5a Merge pull request #4015 from Infisical/dynamic-secrets-doc-links
improvement(frontend): Dynamic secrets doc links
2025-07-14 14:09:14 -07:00
Scott Wilson
268d0d6192 Merge pull request #4013 from Infisical/checkbox-addressal
improvement(frontend): Make checkbox colors more apparent and fix specific priv. checkbox styling
2025-07-14 14:09:01 -07:00
Scott Wilson
7ff8a19518 improvement: address feedback 2025-07-14 14:08:30 -07:00
carlosmonastyrski
1cfb1c2581 Merge pull request #4101 from Infisical/fix/authEnforcedMemberInviteCheck
Fix authEnforced returning a token when org has authEnforced enabled
2025-07-14 18:01:32 -03:00
Carlos Monastyrski
ee7bb2dd4d Fix authEnforced returning a token when org has authEnforced enabled 2025-07-14 14:46:26 -03:00
x032205
513f942aae Add batching to not lock DB 2025-07-14 00:39:34 -04:00
Scott Wilson
944b7b84af chore: revert license 2025-07-11 21:34:47 -07:00
Scott Wilson
32f2a7135c improvement: add overview and provider doc links to all dynamic secrets in modal header (remove one off doc links from dynamic forms) 2025-07-11 21:33:05 -07:00
Scott Wilson
1bab3ecdda fix: correct tw styling 2025-07-11 20:56:38 -07:00
Scott Wilson
eee0be55fd improvement: make checkbox colors more apparent and fix specific privilege checkbox styling 2025-07-11 20:54:23 -07:00
Scott Wilson
221de8beb4 improvements: revise commit history and commit details UI 2025-07-11 20:07:53 -07:00
x032205
218408493a Optimize token cleanup job 2025-07-11 22:05:32 -04:00
Sheen Capadngan
d89418803e misc: update CLI latest version check 2025-07-12 04:31:56 +08:00
247 changed files with 4670 additions and 925 deletions

View File

@@ -149,11 +149,8 @@ Not sure where to get started? You can:
- Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there.
## Resources
## We are hiring!
- [Docs](https://infisical.com/docs/documentation/getting-started/introduction) for comprehensive documentation and guides
- [Slack](https://infisical.com/slack) for discussion with the community and Infisical team.
- [GitHub](https://github.com/Infisical/infisical) for code, issues, and pull requests
- [Twitter](https://twitter.com/infisical) for fast news
- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management
- [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates
If you're reading this, there is a strong chance you like the products we created.
You might also make a great addition to our team. We're growing fast and would love for you to [join us](https://infisical.com/careers).

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.AppConnection, "gatewayId"))) {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.uuid("gatewayId").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.AppConnection, "gatewayId")) {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.dropColumn("gatewayId");
});
}
}

View File

@@ -20,7 +20,8 @@ export const AppConnectionsSchema = z.object({
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional()
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
});
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;

View File

@@ -354,11 +354,17 @@ export const accessApprovalRequestServiceFactory = ({
status === ApprovalStatus.APPROVED;
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
// If user is (not an approver OR cant self approve) AND can't bypass policy
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request."
});
const isSelfRejection = isSelfApproval && status === ApprovalStatus.REJECTED;
// users can always reject (cancel) their own requests
if (!isSelfRejection) {
// If user is (not an approver OR cant self approve) AND can't bypass policy
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request."
});
}
}
if (
@@ -414,7 +420,7 @@ export const accessApprovalRequestServiceFactory = ({
);
// Only throw if actor is not the approver and not bypassing
if (!isApproverOfTheSequence && !isBreakGlassApprovalAttempt) {
if (!isApproverOfTheSequence && !isBreakGlassApprovalAttempt && !isSelfRejection) {
throw new BadRequestError({ message: "You are not a reviewer in this step" });
}
}

View File

@@ -45,7 +45,10 @@ export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion(
]);
export const CreateOracleDBConnectionSchema = ValidateOracleDBConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdateOracleDBConnectionSchema = z
@@ -54,7 +57,12 @@ export const UpdateOracleDBConnectionSchema = z
AppConnections.UPDATE(AppConnection.OracleDB).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const OracleDBConnectionListItemSchema = z.object({
name: z.literal("OracleDB"),

View File

@@ -410,7 +410,7 @@ export const samlConfigServiceFactory = ({
}
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted);
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = crypto.jwt().sign(
{

View File

@@ -4,6 +4,7 @@ import isEqual from "lodash.isequal";
import { SecretType, TableName } from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -107,6 +108,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
queueService: Pick<TQueueServiceFactory, "queuePg">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
};
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
@@ -148,7 +150,8 @@ export const secretRotationV2ServiceFactory = ({
keyStore,
queueService,
folderCommitService,
appConnectionDAL
appConnectionDAL,
gatewayService
}: TSecretRotationV2ServiceFactoryDep) => {
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
const appCfg = getConfig();
@@ -461,7 +464,8 @@ export const secretRotationV2ServiceFactory = ({
rotationInterval: payload.rotationInterval
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
kmsService,
gatewayService
);
// even though we have a db constraint we want to check before any rotation of credentials is attempted
@@ -824,7 +828,8 @@ export const secretRotationV2ServiceFactory = ({
connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
kmsService,
gatewayService
);
const generatedCredentials = await decryptSecretRotationCredentials({
@@ -907,7 +912,8 @@ export const secretRotationV2ServiceFactory = ({
connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
kmsService,
gatewayService
);
const updatedRotation = await rotationFactory.rotateCredentials(

View File

@@ -1,4 +1,5 @@
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
import { OrderByDirection } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
@@ -239,7 +240,8 @@ export type TRotationFactory<
> = (
secretRotation: T,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
issueCredentials: TRotationFactoryIssueCredentials<C, P>;
revokeCredentials: TRotationFactoryRevokeCredentials<C>;

View File

@@ -1,3 +1,5 @@
import { Knex } from "knex";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
@@ -5,7 +7,10 @@ import {
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
import {
executeWithPotentialGateway,
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
} from "@app/services/app-connection/shared/sql";
import { generatePassword } from "../utils";
import {
@@ -30,7 +35,7 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials
> = (secretRotation) => {
> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => {
const {
connection,
parameters: { username1, username2 },
@@ -38,29 +43,38 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
secretsMapping
} = secretRotation;
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
const client = await getSqlConnectionClient({
...connection,
credentials: {
...connection.credentials,
...credentials
}
});
const executeOperation = <T>(
operation: (client: Knex) => Promise<T>,
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
) => {
const finalCredentials = {
...connection.credentials,
...credentialsOverride
};
return executeWithPotentialGateway(
{
...connection,
credentials: finalCredentials
},
gatewayService,
(client) => operation(client)
);
};
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
try {
await client.raw("SELECT 1");
await executeOperation(async (client) => {
await client.raw("SELECT 1");
}, credentials);
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
} finally {
await client.destroy();
}
};
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
callback
) => {
const client = await getSqlConnectionClient(connection);
// For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
const credentialsSet = [
@@ -69,15 +83,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
];
try {
await client.transaction(async (tx) => {
for await (const credentials of credentialsSet) {
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
await executeOperation(async (client) => {
await client.transaction(async (tx) => {
for await (const credentials of credentialsSet) {
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
});
});
} catch (error) {
throw new Error(redactPasswords(error, credentialsSet));
} finally {
await client.destroy();
}
for await (const credentials of credentialsSet) {
@@ -91,21 +105,19 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
credentialsToRevoke,
callback
) => {
const client = await getSqlConnectionClient(connection);
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
try {
await client.transaction(async (tx) => {
for await (const credentials of revokedCredentials) {
// invalidate previous passwords
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
await executeOperation(async (client) => {
await client.transaction(async (tx) => {
for await (const credentials of revokedCredentials) {
// invalidate previous passwords
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
});
});
} catch (error) {
throw new Error(redactPasswords(error, revokedCredentials));
} finally {
await client.destroy();
}
return callback();
@@ -115,17 +127,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
_,
callback
) => {
const client = await getSqlConnectionClient(connection);
// generate new password for the next active user
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
try {
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
await executeOperation(async (client) => {
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
});
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
} finally {
await client.destroy();
}
await $validateCredentials(credentials);

View File

@@ -2282,6 +2282,13 @@ export const AppConnections = {
},
RAILWAY: {
apiToken: "The API token used to authenticate with Railway."
},
CHECKLY: {
apiKey: "The API key used to authenticate with Checkly."
},
SUPABASE: {
accessKey: "The Key used to access Supabase.",
instanceUrl: "The URL used to access Supabase."
}
}
};
@@ -2488,6 +2495,13 @@ export const SecretSyncs = {
environmentName: "The Railway environment to sync secrets to.",
serviceId: "The Railway service that secrets should be synced to.",
serviceName: "The Railway service that secrets should be synced to."
},
CHECKLY: {
accountId: "The ID of the Checkly account to sync secrets to."
},
SUPABASE: {
projectId: "The ID of the Supabase project to sync secrets to.",
projectName: "The name of the Supabase project to sync secrets to."
}
}
};

View File

@@ -93,7 +93,13 @@ const cryptographyFactory = () => {
};
const verifyFipsLicense = (licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">) => {
if (isFipsModeEnabled({ skipInitializationCheck: true }) && !licenseService.onPremFeatures?.fips) {
const appCfg = getConfig();
if (
!appCfg.isDevelopmentMode &&
isFipsModeEnabled({ skipInitializationCheck: true }) &&
!licenseService.onPremFeatures?.fips
) {
throw new CryptographyError({
message: "FIPS mode is enabled but your license does not include FIPS support. Please contact support."
});

View File

@@ -0,0 +1,43 @@
import { FastifyReply } from "fastify";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
/**
* `aod` (Auth Origin Domain) cookie is used to store the origin domain of the application when user was last authenticated.
* This is useful for determining the target domain for authentication redirects, especially in cloud deployments.
* It is set only in cloud mode to ensure that the cookie is shared across subdomains.
*/
export function addAuthOriginDomainCookie(res: FastifyReply) {
try {
const appCfg = getConfig();
// Only set the cookie if the app is running in cloud mode
if (!appCfg.isCloud) return;
const siteUrl = appCfg.SITE_URL!;
let domain: string;
const { hostname } = new URL(siteUrl);
const parts = hostname.split(".");
if (parts.length >= 2) {
// For `app.infisical.com` => `.infisical.com`
domain = `.${parts.slice(-2).join(".")}`;
} else {
// If somehow only "example", fallback to itself
domain = `.${hostname}`;
}
void res.setCookie("aod", siteUrl, {
domain,
path: "/",
sameSite: "strict",
httpOnly: false,
secure: appCfg.HTTPS_ENABLED
});
} catch (error) {
logger.error(error, "Failed to set auth origin domain cookie");
}
}

View File

@@ -1706,7 +1706,9 @@ export const registerRoutes = async (
appConnectionDAL,
permissionService,
kmsService,
licenseService
licenseService,
gatewayService,
gatewayDAL
});
const secretSyncService = secretSyncServiceFactory({
@@ -1804,7 +1806,8 @@ export const registerRoutes = async (
snapshotService,
secretQueueService,
queueService,
appConnectionDAL
appConnectionDAL,
gatewayService
});
const certificateAuthorityService = certificateAuthorityServiceFactory({

View File

@@ -12,6 +12,7 @@ import { getConfig, overridableKeys } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -593,6 +594,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
message: "Successfully set up admin account",
user: user.user,

View File

@@ -25,12 +25,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
credentials: I["credentials"];
description?: string | null;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
}>;
updateSchema: z.ZodType<{
name?: string;
credentials?: I["credentials"];
description?: string | null;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
}>;
sanitizedResponseSchema: z.ZodTypeAny;
}) => {
@@ -224,10 +226,10 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, method, credentials, description, isPlatformManagedCredentials } = req.body;
const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description, isPlatformManagedCredentials },
{ name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId },
req.permission
)) as T;
@@ -270,11 +272,11 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, credentials, description, isPlatformManagedCredentials } = req.body;
const { name, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body;
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.updateAppConnection(
{ name, credentials, connectionId, description, isPlatformManagedCredentials },
{ name, credentials, connectionId, description, isPlatformManagedCredentials, gatewayId },
req.permission
)) as T;

View File

@@ -39,6 +39,10 @@ import {
CamundaConnectionListItemSchema,
SanitizedCamundaConnectionSchema
} from "@app/services/app-connection/camunda";
import {
ChecklyConnectionListItemSchema,
SanitizedChecklyConnectionSchema
} from "@app/services/app-connection/checkly";
import {
CloudflareConnectionListItemSchema,
SanitizedCloudflareConnectionSchema
@@ -79,6 +83,10 @@ import {
RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema
} from "@app/services/app-connection/render/render-connection-schema";
import {
SanitizedSupabaseConnectionSchema,
SupabaseConnectionListItemSchema
} from "@app/services/app-connection/supabase";
import {
SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema
@@ -128,7 +136,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedCloudflareConnectionSchema.options,
...SanitizedBitbucketConnectionSchema.options,
...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options
...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -163,7 +173,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
CloudflareConnectionListItemSchema,
BitbucketConnectionListItemSchema,
ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema
RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,56 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateChecklyConnectionSchema,
SanitizedChecklyConnectionSchema,
UpdateChecklyConnectionSchema
} from "@app/services/app-connection/checkly";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerChecklyConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Checkly,
server,
sanitizedResponseSchema: SanitizedChecklyConnectionSchema,
createSchema: CreateChecklyConnectionSchema,
updateSchema: UpdateChecklyConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/accounts`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
accounts: z
.object({
name: z.string(),
id: z.string(),
runtimeId: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const accounts = await server.services.appConnection.checkly.listAccounts(connectionId, req.permission);
return { accounts };
}
});
};

View File

@@ -11,6 +11,7 @@ import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-r
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
@@ -27,6 +28,7 @@ import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router";
@@ -68,5 +70,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter
[AppConnection.Railway]: registerRailwayConnectionRouter,
[AppConnection.Checkly]: registerChecklyConnectionRouter,
[AppConnection.Supabase]: registerSupabaseConnectionRouter
};

View File

@@ -0,0 +1,55 @@
import { z } from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateSupabaseConnectionSchema,
SanitizedSupabaseConnectionSchema,
UpdateSupabaseConnectionSchema
} from "@app/services/app-connection/supabase";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerSupabaseConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Supabase,
server,
sanitizedResponseSchema: SanitizedSupabaseConnectionSchema,
createSchema: CreateSupabaseConnectionSchema,
updateSchema: UpdateSupabaseConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
projects: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.supabase.listProjects(connectionId, req.permission);
return { projects };
}
});
};

View File

@@ -42,6 +42,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
maxAge: 0
});
void res.cookie("aod", "", {
httpOnly: false,
path: "/",
sameSite: "lax",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return { message: "Successfully logged out" };
}
});

View File

@@ -28,7 +28,17 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
.object({
authorization: z.string(),
host: z.string(),
"x-date": z.string()
"x-date": z.string().optional(),
date: z.string().optional()
})
.superRefine((val, ctx) => {
if (!val.date && !val["x-date"]) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Either date or x-date must be provided",
path: ["headers", "date"]
});
}
})
.describe(OCI_AUTH.LOGIN.headers)
}),

View File

@@ -0,0 +1,17 @@
import {
ChecklySyncSchema,
CreateChecklySyncSchema,
UpdateChecklySyncSchema
} from "@app/services/secret-sync/checkly/checkly-sync-schemas";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerChecklySyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Checkly,
server,
responseSchema: ChecklySyncSchema,
createSchema: CreateChecklySyncSchema,
updateSchema: UpdateChecklySyncSchema
});

View File

@@ -8,6 +8,7 @@ import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configurati
import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerCamundaSyncRouter } from "./camunda-sync-router";
import { registerChecklySyncRouter } from "./checkly-sync-router";
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
@@ -20,6 +21,7 @@ import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router";
import { registerSupabaseSyncRouter } from "./supabase-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router";
@@ -52,7 +54,8 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.GitLab]: registerGitLabSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter,
[SecretSync.CloudflareWorkers]: registerCloudflareWorkersSyncRouter,
[SecretSync.Supabase]: registerSupabaseSyncRouter,
[SecretSync.Zabbix]: registerZabbixSyncRouter,
[SecretSync.Railway]: registerRailwaySyncRouter
[SecretSync.Railway]: registerRailwaySyncRouter,
[SecretSync.Checkly]: registerChecklySyncRouter
};

View File

@@ -22,6 +22,7 @@ import {
import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/services/secret-sync/azure-devops";
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
import {
CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema
@@ -40,6 +41,7 @@ import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -70,9 +72,10 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
GitLabSyncSchema,
CloudflarePagesSyncSchema,
CloudflareWorkersSyncSchema,
SupabaseSyncSchema,
ZabbixSyncSchema,
RailwaySyncSchema
RailwaySyncSchema,
ChecklySyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -101,7 +104,9 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
CloudflareWorkersSyncListItemSchema,
ZabbixSyncListItemSchema,
RailwaySyncListItemSchema
RailwaySyncListItemSchema,
ChecklySyncListItemSchema,
SupabaseSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,17 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
CreateSupabaseSyncSchema,
SupabaseSyncSchema,
UpdateSupabaseSyncSchema
} from "@app/services/secret-sync/supabase";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerSupabaseSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Supabase,
server,
responseSchema: SupabaseSyncSchema,
createSchema: CreateSupabaseSyncSchema,
updateSchema: UpdateSupabaseSyncSchema
});

View File

@@ -22,6 +22,7 @@ import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms";
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -475,6 +476,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,

View File

@@ -4,6 +4,7 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { mfaRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
export const registerMfaRouter = async (server: FastifyZodProvider) => {
@@ -131,6 +132,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
...user,
token: token.access,

View File

@@ -10,6 +10,7 @@ import {
import { ApiDocsTags, ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -396,6 +397,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return { organization, accessToken: tokens.accessToken };
}
});

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
export const registerLoginRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -93,6 +94,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
@@ -155,6 +158,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",

View File

@@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors";
import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -170,6 +171,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return { message: "Successfully set up account", user, token: accessToken, organizationId };
}
});
@@ -239,6 +242,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
});
// TODO(akhilmhdh-pg): add telemetry service
addAuthOriginDomainCookie(res);
return { message: "Successfully set up account", user, token: accessToken };
}
});

View File

@@ -31,12 +31,16 @@ export const validateOnePassConnectionCredentials = async (config: TOnePassConne
const { apiToken } = config.credentials;
try {
await request.get(`${instanceUrl}/v1/vaults`, {
const res = await request.get(`${instanceUrl}/v1/vaults`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
if (!Array.isArray(res.data)) {
throw new AxiosError("Invalid response from 1Password API");
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({

View File

@@ -30,7 +30,9 @@ export enum AppConnection {
Cloudflare = "cloudflare",
Zabbix = "zabbix",
Railway = "railway",
Bitbucket = "bitbucket"
Bitbucket = "bitbucket",
Checkly = "checkly",
Supabase = "supabase"
}
export enum AWSRegion {

View File

@@ -5,6 +5,7 @@ import {
validateOCIConnectionCredentials
} from "@app/ee/services/app-connections/oci";
import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
@@ -56,6 +57,7 @@ import {
validateBitbucketConnectionCredentials
} from "./bitbucket";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
import {
getCloudflareConnectionListItem,
@@ -94,6 +96,11 @@ import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postg
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import {
getSupabaseConnectionListItem,
SupabaseConnectionMethod,
validateSupabaseConnectionCredentials
} from "./supabase";
import {
getTeamCityConnectionListItem,
TeamCityConnectionMethod,
@@ -146,7 +153,9 @@ export const listAppConnectionOptions = () => {
getCloudflareConnectionListItem(),
getZabbixConnectionListItem(),
getRailwayConnectionListItem(),
getBitbucketConnectionListItem()
getBitbucketConnectionListItem(),
getChecklyConnectionListItem(),
getSupabaseConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -193,7 +202,8 @@ export const decryptAppConnectionCredentials = async ({
};
export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig
appConnection: TAppConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<TAppConnection["credentials"]> => {
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -229,10 +239,12 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
};
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
@@ -287,7 +299,10 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case LdapConnectionMethod.SimpleBind:
return "Simple Bind";
case RenderConnectionMethod.ApiKey:
case ChecklyConnectionMethod.ApiKey:
return "API Key";
case SupabaseConnectionMethod.AccessToken:
return "Access Token";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -350,7 +365,9 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
[AppConnection.Supabase]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (

View File

@@ -32,7 +32,9 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.Zabbix]: "Zabbix",
[AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket"
[AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Checkly]: "Checkly",
[AppConnection.Supabase]: "Supabase"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -67,5 +69,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
[AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
[AppConnection.Supabase]: AppConnectionPlanType.Regular
};

View File

@@ -18,7 +18,7 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
export const GenericCreateAppConnectionFieldsSchema = (
app: AppConnection,
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
{ supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {}
) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
@@ -30,12 +30,23 @@ export const GenericCreateAppConnectionFieldsSchema = (
.describe(AppConnections.CREATE(app).description),
isPlatformManagedCredentials: supportsPlatformManagedCredentials
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
: z
.literal(false, {
errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
})
.optional()
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`),
gatewayId: supportsGateways
? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.")
: z
.undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
.or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }))
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
});
export const GenericUpdateAppConnectionFieldsSchema = (
app: AppConnection,
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
{ supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {}
) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
@@ -47,5 +58,16 @@ export const GenericUpdateAppConnectionFieldsSchema = (
.describe(AppConnections.UPDATE(app).description),
isPlatformManagedCredentials: supportsPlatformManagedCredentials
? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials)
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
: z
.literal(false, {
errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
})
.optional()
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`),
gatewayId: supportsGateways
? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.")
: z
.undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
.or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }))
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
});

View File

@@ -3,8 +3,14 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
OrgPermissionAppConnectionActions,
OrgPermissionGatewayActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography";
import { DatabaseErrorCode } from "@app/lib/error-codes";
@@ -49,6 +55,8 @@ import { ValidateBitbucketConnectionCredentialsSchema } from "./bitbucket";
import { bitbucketConnectionService } from "./bitbucket/bitbucket-connection-service";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service";
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
import { checklyConnectionService } from "./checkly/checkly-connection-service";
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
@@ -76,6 +84,8 @@ import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service";
import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase";
import { supabaseConnectionService } from "./supabase/supabase-connection-service";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
@@ -92,6 +102,8 @@ export type TAppConnectionServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
};
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -128,14 +140,18 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
appConnectionDAL,
permissionService,
kmsService,
licenseService
licenseService,
gatewayService,
gatewayDAL
}: TAppConnectionServiceFactoryDep) => {
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
const { permission } = await permissionService.getOrgPermission(
@@ -216,7 +232,7 @@ export const appConnectionServiceFactory = ({
};
const createAppConnection = async (
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
{ method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO,
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getOrgPermission(
@@ -232,6 +248,20 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections
);
if (gatewayId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found for org`
});
}
}
await enterpriseAppCheck(
licenseService,
app,
@@ -239,12 +269,16 @@ export const appConnectionServiceFactory = ({
"Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections."
);
const validatedCredentials = await validateAppConnectionCredentials({
app,
credentials,
method,
orgId: actor.orgId
} as TAppConnectionConfig);
const validatedCredentials = await validateAppConnectionCredentials(
{
app,
credentials,
method,
orgId: actor.orgId,
gatewayId
} as TAppConnectionConfig,
gatewayService
);
try {
const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
@@ -259,6 +293,7 @@ export const appConnectionServiceFactory = ({
encryptedCredentials,
method,
app,
gatewayId,
...params
});
};
@@ -271,9 +306,11 @@ export const appConnectionServiceFactory = ({
app,
orgId: actor.orgId,
credentials: validatedCredentials,
method
method,
gatewayId
} as TAppConnectionConfig,
(platformCredentials) => createConnection(platformCredentials)
(platformCredentials) => createConnection(platformCredentials),
gatewayService
);
} else {
connection = await createConnection(validatedCredentials);
@@ -294,7 +331,7 @@ export const appConnectionServiceFactory = ({
};
const updateAppConnection = async (
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
{ connectionId, credentials, gatewayId, ...params }: TUpdateAppConnectionDTO,
actor: OrgServiceActor
) => {
const appConnection = await appConnectionDAL.findById(connectionId);
@@ -321,6 +358,22 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections
);
if (gatewayId !== appConnection.gatewayId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found for org`
});
}
}
}
// prevent updating credentials or management status if platform managed
if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) {
throw new BadRequestError({
@@ -345,12 +398,16 @@ export const appConnectionServiceFactory = ({
} Connection with method ${getAppConnectionMethodName(method)}`
});
updatedCredentials = await validateAppConnectionCredentials({
app,
orgId: actor.orgId,
credentials,
method
} as TAppConnectionConfig);
updatedCredentials = await validateAppConnectionCredentials(
{
app,
orgId: actor.orgId,
credentials,
method,
gatewayId
} as TAppConnectionConfig,
gatewayService
);
if (!updatedCredentials)
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
@@ -369,6 +426,7 @@ export const appConnectionServiceFactory = ({
return appConnectionDAL.updateById(connectionId, {
orgId: actor.orgId,
encryptedCredentials,
gatewayId,
...params
});
};
@@ -385,9 +443,11 @@ export const appConnectionServiceFactory = ({
app,
orgId: actor.orgId,
credentials: updatedCredentials,
method
method,
gatewayId
} as TAppConnectionConfig,
(platformCredentials) => updateConnection(platformCredentials)
(platformCredentials) => updateConnection(platformCredentials),
gatewayService
);
} else {
updatedConnection = await updateConnection(updatedCredentials);
@@ -541,6 +601,8 @@ export const appConnectionServiceFactory = ({
cloudflare: cloudflareConnectionService(connectAppConnectionById),
zabbix: zabbixConnectionService(connectAppConnectionById),
railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById)
bitbucket: bitbucketConnectionService(connectAppConnectionById),
checkly: checklyConnectionService(connectAppConnectionById),
supabase: supabaseConnectionService(connectAppConnectionById)
};
};

View File

@@ -9,6 +9,7 @@ import {
TOracleDBConnectionInput,
TValidateOracleDBConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oracledb";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -68,6 +69,12 @@ import {
TCamundaConnectionInput,
TValidateCamundaConnectionCredentialsSchema
} from "./camunda";
import {
TChecklyConnection,
TChecklyConnectionConfig,
TChecklyConnectionInput,
TValidateChecklyConnectionCredentialsSchema
} from "./checkly";
import {
TCloudflareConnection,
TCloudflareConnectionConfig,
@@ -153,6 +160,12 @@ import {
TRenderConnectionInput,
TValidateRenderConnectionCredentialsSchema
} from "./render/render-connection-types";
import {
TSupabaseConnection,
TSupabaseConnectionConfig,
TSupabaseConnectionInput,
TValidateSupabaseConnectionCredentialsSchema
} from "./supabase";
import {
TTeamCityConnection,
TTeamCityConnectionConfig,
@@ -217,6 +230,8 @@ export type TAppConnection = { id: string } & (
| TBitbucketConnection
| TZabbixConnection
| TRailwayConnection
| TChecklyConnection
| TSupabaseConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -256,6 +271,8 @@ export type TAppConnectionInput = { id: string } & (
| TBitbucketConnectionInput
| TZabbixConnectionInput
| TRailwayConnectionInput
| TChecklyConnectionInput
| TSupabaseConnectionInput
);
export type TSqlConnectionInput =
@@ -266,7 +283,7 @@ export type TSqlConnectionInput =
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials"
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId"
>;
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
@@ -302,7 +319,9 @@ export type TAppConnectionConfig =
| TCloudflareConnectionConfig
| TBitbucketConnectionConfig
| TZabbixConnectionConfig
| TRailwayConnectionConfig;
| TRailwayConnectionConfig
| TChecklyConnectionConfig
| TSupabaseConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -336,7 +355,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateCloudflareConnectionCredentialsSchema
| TValidateBitbucketConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema;
| TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema
| TValidateSupabaseConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -349,14 +370,17 @@ export type TListAwsConnectionIamUsers = {
};
export type TAppConnectionCredentialsValidator = (
appConnection: TAppConnectionConfig
appConnection: TAppConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => Promise<TAppConnection["credentials"]>;
export type TAppConnectionTransitionCredentialsToPlatform = (
appConnection: TAppConnectionConfig,
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => Promise<TAppConnectionRaw>;
export type TAppConnectionBaseConfig = {
supportsPlatformManagedCredentials?: boolean;
supportsGateways?: boolean;
};

View File

@@ -0,0 +1,3 @@
export enum ChecklyConnectionMethod {
ApiKey = "api-key"
}

View File

@@ -0,0 +1,35 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
import { ChecklyPublicAPI } from "./checkly-connection-public-client";
import { TChecklyConnectionConfig } from "./checkly-connection-types";
export const getChecklyConnectionListItem = () => {
return {
name: "Checkly" as const,
app: AppConnection.Checkly as const,
methods: Object.values(ChecklyConnectionMethod)
};
};
export const validateChecklyConnectionCredentials = async (config: TChecklyConnectionConfig) => {
try {
await ChecklyPublicAPI.healthcheck(config);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection - verify credentials"
});
}
return config.credentials;
};

View File

@@ -0,0 +1,186 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable class-methods-use-this */
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxiosError } from "axios";
import { createRequestClient } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types";
export function getChecklyAuthHeaders(
connection: TChecklyConnectionConfig,
accountId?: string
): Record<string, string> {
switch (connection.method) {
case ChecklyConnectionMethod.ApiKey:
return {
Authorization: `Bearer ${connection.credentials.apiKey}`,
...(accountId && { "X-Checkly-Account": accountId })
};
default:
throw new Error(`Unsupported Checkly connection method`);
}
}
export function getChecklyRatelimiter(response: AxiosResponse): {
maxAttempts: number;
isRatelimited: boolean;
wait: () => Promise<void>;
} {
const wait = () => {
return new Promise<void>((res) => {
setTimeout(res, 60 * 1000); // Wait for 60 seconds
});
};
return {
isRatelimited: response.status === HttpStatusCode.TooManyRequests,
wait,
maxAttempts: 3
};
}
class ChecklyPublicClient {
private client: AxiosInstance;
constructor() {
this.client = createRequestClient({
baseURL: IntegrationUrls.CHECKLY_API_URL,
headers: {
"Content-Type": "application/json"
}
});
}
async send<T>(
connection: TChecklyConnectionConfig,
config: AxiosRequestConfig & { accountId?: string },
retryAttempt = 0
): Promise<T | undefined> {
const response = await this.client.request<T>({
...config,
timeout: 1000 * 60, // 60 seconds timeout
validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests,
headers: getChecklyAuthHeaders(connection, config.accountId)
});
const limiter = getChecklyRatelimiter(response);
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
await limiter.wait();
return this.send(connection, config, retryAttempt + 1);
}
return response.data;
}
healthcheck(connection: TChecklyConnectionConfig) {
switch (connection.method) {
case ChecklyConnectionMethod.ApiKey:
return this.getChecklyAccounts(connection);
default:
throw new Error(`Unsupported Checkly connection method`);
}
}
async getVariables(connection: TChecklyConnectionConfig, accountId: string, limit: number = 50, page: number = 1) {
const res = await this.send<TChecklyVariable[]>(connection, {
accountId,
method: "GET",
url: `/v1/variables`,
params: {
limit,
page
}
});
return res;
}
async createVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "POST",
url: `/v1/variables`,
data: variable
});
return res;
}
async updateVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "PUT",
url: `/v1/variables/${variable.key}`,
data: variable
});
return res;
}
async getVariable(connection: TChecklyConnectionConfig, accountId: string, variable: Pick<TChecklyVariable, "key">) {
try {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "GET",
url: `/v1/variables/${variable.key}`
});
return res;
} catch (error) {
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
return null;
}
throw error;
}
}
async upsertVariable(connection: TChecklyConnectionConfig, accountId: string, variable: TChecklyVariable) {
const res = await this.getVariable(connection, accountId, variable);
if (!res) {
return this.createVariable(connection, accountId, variable);
}
await this.updateVariable(connection, accountId, variable);
return res;
}
async deleteVariable(
connection: TChecklyConnectionConfig,
accountId: string,
variable: Pick<TChecklyVariable, "key">
) {
try {
const res = await this.send<TChecklyVariable>(connection, {
accountId,
method: "DELETE",
url: `/v1/variables/${variable.key}`
});
return res;
} catch (error) {
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
return null;
}
throw error;
}
}
async getChecklyAccounts(connection: TChecklyConnectionConfig) {
// This endpoint is in beta and might be subject to changes
// Refer: https://developers.checklyhq.com/reference/getv1accounts
const res = await this.send<TChecklyAccount[]>(connection, {
method: "GET",
url: `/v1/accounts`
});
return res;
}
}
export const ChecklyPublicAPI = new ChecklyPublicClient();

View File

@@ -0,0 +1,62 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
export const ChecklyConnectionMethodSchema = z
.nativeEnum(ChecklyConnectionMethod)
.describe(AppConnections.CREATE(AppConnection.Checkly).method);
export const ChecklyConnectionAccessTokenCredentialsSchema = z.object({
apiKey: z.string().trim().min(1, "API Key required").max(255).describe(AppConnections.CREDENTIALS.CHECKLY.apiKey)
});
const BaseChecklyConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Checkly)
});
export const ChecklyConnectionSchema = BaseChecklyConnectionSchema.extend({
method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema
});
export const SanitizedChecklyConnectionSchema = z.discriminatedUnion("method", [
BaseChecklyConnectionSchema.extend({
method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateChecklyConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Checkly).credentials
)
})
]);
export const CreateChecklyConnectionSchema = ValidateChecklyConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Checkly)
);
export const UpdateChecklyConnectionSchema = z
.object({
credentials: ChecklyConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Checkly).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Checkly));
export const ChecklyConnectionListItemSchema = z.object({
name: z.literal("Checkly"),
app: z.literal(AppConnection.Checkly),
methods: z.nativeEnum(ChecklyConnectionMethod).array()
});

View File

@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { ChecklyPublicAPI } from "./checkly-connection-public-client";
import { TChecklyConnection } from "./checkly-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TChecklyConnection>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const checklyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listAccounts = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Checkly, connectionId, actor);
try {
const accounts = await ChecklyPublicAPI.getChecklyAccounts(appConnection);
return accounts!;
} catch (error) {
logger.error(error, "Failed to list accounts on Checkly");
return [];
}
};
return {
listAccounts
};
};

View File

@@ -0,0 +1,35 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
ChecklyConnectionSchema,
CreateChecklyConnectionSchema,
ValidateChecklyConnectionCredentialsSchema
} from "./checkly-connection-schemas";
export type TChecklyConnection = z.infer<typeof ChecklyConnectionSchema>;
export type TChecklyConnectionInput = z.infer<typeof CreateChecklyConnectionSchema> & {
app: AppConnection.Checkly;
};
export type TValidateChecklyConnectionCredentialsSchema = typeof ValidateChecklyConnectionCredentialsSchema;
export type TChecklyConnectionConfig = DiscriminativePick<TChecklyConnection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TChecklyVariable = {
key: string;
value: string;
locked: boolean;
secret: boolean;
};
export type TChecklyAccount = {
id: string;
name: string;
runtimeId: string;
};

View File

@@ -0,0 +1,4 @@
export * from "./checkly-connection-constants";
export * from "./checkly-connection-fns";
export * from "./checkly-connection-schemas";
export * from "./checkly-connection-types";

View File

@@ -9,6 +9,7 @@ import { getAppConnectionMethodName } from "@app/services/app-connection/app-con
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { GithubTokenRespData, isGithubErrorResponse } from "../github/github-connection-fns";
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
import {
TGitHubRadarConnection,
@@ -71,13 +72,6 @@ export const listGitHubRadarRepositories = async (appConnection: TGitHubRadarCon
return repositories;
};
type TokenRespData = {
access_token: string;
scope: string;
token_type: string;
error?: string;
};
export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRadarConnectionConfig) => {
const { credentials, method } = config;
@@ -93,10 +87,10 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
});
}
let tokenResp: AxiosResponse<TokenRespData>;
let tokenResp: AxiosResponse<GithubTokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID,
client_secret: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET,
@@ -108,19 +102,27 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
"Accept-Encoding": "application/json"
}
});
if (isGithubErrorResponse(tokenResp?.data)) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
});
}
} catch (e: unknown) {
if (e instanceof BadRequestError) {
throw e;
}
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
}
if (tokenResp.status !== 200) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
});
}
if (method === GitHubRadarConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{
installations: {
id: number;
@@ -149,10 +151,6 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubRadarConnectionMethod.App:
return {

View File

@@ -144,13 +144,21 @@ export const getGitHubEnvironments = async (appConnection: TGitHubConnection, ow
}
};
type TokenRespData = {
access_token: string;
export type GithubTokenRespData = {
access_token?: string;
scope: string;
token_type: string;
error?: string;
};
export function isGithubErrorResponse(data: GithubTokenRespData): data is GithubTokenRespData & {
error: string;
error_description: string;
error_uri: string;
} {
return "error" in data;
}
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config;
@@ -183,10 +191,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
});
}
let tokenResp: AxiosResponse<TokenRespData>;
let tokenResp: AxiosResponse<GithubTokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: clientId,
client_secret: clientSecret,
@@ -198,7 +206,17 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
"Accept-Encoding": "application/json"
}
});
if (isGithubErrorResponse(tokenResp?.data)) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
});
}
} catch (e: unknown) {
if (e instanceof BadRequestError) {
throw e;
}
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
@@ -211,6 +229,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
if (method === GitHubConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{
installations: {
id: number;
@@ -239,10 +261,6 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubConnectionMethod.App:
return {

View File

@@ -49,7 +49,10 @@ export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("me
]);
export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdateMsSqlConnectionSchema = z
@@ -58,7 +61,12 @@ export const UpdateMsSqlConnectionSchema = z
AppConnections.UPDATE(AppConnection.MsSql).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const MsSqlConnectionListItemSchema = z.object({
name: z.literal("Microsoft SQL Server"),

View File

@@ -47,7 +47,10 @@ export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("me
]);
export const CreateMySqlConnectionSchema = ValidateMySqlConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdateMySqlConnectionSchema = z
@@ -56,7 +59,12 @@ export const UpdateMySqlConnectionSchema = z
AppConnections.UPDATE(AppConnection.MySql).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const MySqlConnectionListItemSchema = z.object({
name: z.literal("MySQL"),

View File

@@ -47,7 +47,10 @@ export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion(
]);
export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdatePostgresConnectionSchema = z
@@ -56,7 +59,12 @@ export const UpdatePostgresConnectionSchema = z
AppConnections.UPDATE(AppConnection.Postgres).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const PostgresConnectionListItemSchema = z.object({
name: z.literal("PostgreSQL"),

View File

@@ -1,11 +1,13 @@
import knex, { Knex } from "knex";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import {
TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types";
@@ -98,25 +100,80 @@ export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection,
return client;
};
export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => {
const { credentials, app } = config;
export const executeWithPotentialGateway = async <T>(
config: TSqlConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
operation: (client: Knex) => Promise<T>
): Promise<T> => {
const { credentials, app, gatewayId } = config;
let client: Knex | undefined;
if (gatewayId && gatewayService) {
const [targetHost] = await verifyHostInputValidity(credentials.host, true);
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
return withGatewayProxy(
async (proxyPort) => {
const client = knex({
client: SQL_CONNECTION_CLIENT_MAP[app],
connection: {
database: credentials.database,
port: proxyPort,
host: "localhost",
user: credentials.username,
password: credentials.password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
...getConnectionConfig({ app, credentials })
}
});
try {
return await operation(client);
} finally {
await client.destroy();
}
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost,
targetPort: credentials.port,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
}
// Non-gateway path
const client = await getSqlConnectionClient({ app, credentials });
try {
client = await getSqlConnectionClient({ app, credentials });
return await operation(client);
} finally {
await client.destroy();
}
};
await client.raw(`Select 1`);
return credentials;
export const validateSqlConnectionCredentials = async (
config: TSqlConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
try {
await executeWithPotentialGateway(config, gatewayService, async (client) => {
await client.raw(`Select 1`);
});
return config.credentials;
} catch (error) {
throw new BadRequestError({
message: `Unable to validate connection: ${
(error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials"
(error as Error)?.message?.replaceAll(config.credentials.password, "********************") ??
"verify credentials"
}`
});
} finally {
await client?.destroy();
}
};
@@ -132,22 +189,23 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record<
export const transferSqlConnectionCredentialsToPlatform = async (
config: TSqlConnectionConfig,
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const { credentials, app } = config;
const client = await getSqlConnectionClient({ app, credentials });
const newPassword = alphaNumericNanoId(32);
try {
return await client.transaction(async (tx) => {
await tx.raw(
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
);
return callback({
...credentials,
password: newPassword
return await executeWithPotentialGateway(config, gatewayService, (client) => {
return client.transaction(async (tx) => {
await tx.raw(
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
);
return callback({
...credentials,
password: newPassword
});
});
});
} catch (error) {
@@ -161,7 +219,5 @@ export const transferSqlConnectionCredentialsToPlatform = async (
(error as Error)?.message?.replaceAll(newPassword, "********************") ??
"Encountered an error transferring credentials to platform"
});
} finally {
await client.destroy();
}
};

View File

@@ -1,6 +1,9 @@
import { DiscriminativePick } from "@app/lib/types";
import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types";
export type TSqlConnectionConfig = DiscriminativePick<TSqlConnectionInput, "method" | "app" | "credentials"> & {
export type TSqlConnectionConfig = DiscriminativePick<
TSqlConnectionInput,
"method" | "app" | "credentials" | "gatewayId"
> & {
orgId: string;
};

View File

@@ -0,0 +1,4 @@
export * from "./supabase-connection-constants";
export * from "./supabase-connection-fns";
export * from "./supabase-connection-schemas";
export * from "./supabase-connection-types";

View File

@@ -0,0 +1,3 @@
export enum SupabaseConnectionMethod {
AccessToken = "access-token"
}

View File

@@ -0,0 +1,58 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
import { SupabasePublicAPI } from "./supabase-connection-public-client";
import { TSupabaseConnection, TSupabaseConnectionConfig } from "./supabase-connection-types";
export const getSupabaseConnectionListItem = () => {
return {
name: "Supabase" as const,
app: AppConnection.Supabase as const,
methods: Object.values(SupabaseConnectionMethod)
};
};
export const validateSupabaseConnectionCredentials = async (config: TSupabaseConnectionConfig) => {
const { credentials } = config;
try {
await SupabasePublicAPI.healthcheck(config);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection - verify credentials"
});
}
return credentials;
};
export const listProjects = async (appConnection: TSupabaseConnection) => {
try {
return await SupabasePublicAPI.getProjects(appConnection);
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list projects: ${error.message || "Unknown error"}`
});
}
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: "Unable to list projects",
error
});
}
};

View File

@@ -0,0 +1,133 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable class-methods-use-this */
import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode } from "axios";
import { createRequestClient } from "@app/lib/config/request";
import { delay } from "@app/lib/delay";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
import { TSupabaseConnectionConfig, TSupabaseProject, TSupabaseSecret } from "./supabase-connection-types";
export const getSupabaseInstanceUrl = async (config: TSupabaseConnectionConfig) => {
const instanceUrl = config.credentials.instanceUrl
? removeTrailingSlash(config.credentials.instanceUrl)
: "https://api.supabase.com";
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export function getSupabaseAuthHeaders(connection: TSupabaseConnectionConfig): Record<string, string> {
switch (connection.method) {
case SupabaseConnectionMethod.AccessToken:
return {
Authorization: `Bearer ${connection.credentials.accessKey}`
};
default:
throw new Error(`Unsupported Supabase connection method`);
}
}
export function getSupabaseRatelimiter(response: AxiosResponse): {
maxAttempts: number;
isRatelimited: boolean;
wait: () => Promise<void>;
} {
const wait = () => {
return delay(60 * 1000);
};
return {
isRatelimited: response.status === HttpStatusCode.TooManyRequests,
wait,
maxAttempts: 3
};
}
class SupabasePublicClient {
private client: AxiosInstance;
constructor() {
this.client = createRequestClient({
headers: {
"Content-Type": "application/json"
}
});
}
async send<T>(
connection: TSupabaseConnectionConfig,
config: AxiosRequestConfig,
retryAttempt = 0
): Promise<T | undefined> {
const response = await this.client.request<T>({
...config,
baseURL: await getSupabaseInstanceUrl(connection),
validateStatus: (status) => (status >= 200 && status < 300) || status === HttpStatusCode.TooManyRequests,
headers: getSupabaseAuthHeaders(connection)
});
const limiter = getSupabaseRatelimiter(response);
if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) {
await limiter.wait();
return this.send(connection, config, retryAttempt + 1);
}
return response.data;
}
async healthcheck(connection: TSupabaseConnectionConfig) {
switch (connection.method) {
case SupabaseConnectionMethod.AccessToken:
return void (await this.getProjects(connection));
default:
throw new Error(`Unsupported Supabase connection method`);
}
}
async getVariables(connection: TSupabaseConnectionConfig, projectRef: string) {
const res = await this.send<TSupabaseSecret[]>(connection, {
method: "GET",
url: `/v1/projects/${projectRef}/secrets`
});
return res;
}
// Supabase does not support updating variables directly
// Instead, just call create again with the same key and it will overwrite the existing variable
async createVariables(connection: TSupabaseConnectionConfig, projectRef: string, ...variables: TSupabaseSecret[]) {
const res = await this.send<TSupabaseSecret>(connection, {
method: "POST",
url: `/v1/projects/${projectRef}/secrets`,
data: variables
});
return res;
}
async deleteVariables(connection: TSupabaseConnectionConfig, projectRef: string, ...variables: string[]) {
const res = await this.send(connection, {
method: "DELETE",
url: `/v1/projects/${projectRef}/secrets`,
data: variables
});
return res;
}
async getProjects(connection: TSupabaseConnectionConfig) {
const res = await this.send<TSupabaseProject[]>(connection, {
method: "GET",
url: `/v1/projects`
});
return res;
}
}
export const SupabasePublicAPI = new SupabasePublicClient();

View File

@@ -0,0 +1,70 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { SupabaseConnectionMethod } from "./supabase-connection-constants";
export const SupabaseConnectionMethodSchema = z
.nativeEnum(SupabaseConnectionMethod)
.describe(AppConnections.CREATE(AppConnection.Supabase).method);
export const SupabaseConnectionAccessTokenCredentialsSchema = z.object({
accessKey: z
.string()
.trim()
.min(1, "Access Key required")
.max(255)
.describe(AppConnections.CREDENTIALS.SUPABASE.accessKey),
instanceUrl: z.string().trim().url().max(255).describe(AppConnections.CREDENTIALS.SUPABASE.instanceUrl).optional()
});
const BaseSupabaseConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Supabase)
});
export const SupabaseConnectionSchema = BaseSupabaseConnectionSchema.extend({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema
});
export const SanitizedSupabaseConnectionSchema = z.discriminatedUnion("method", [
BaseSupabaseConnectionSchema.extend({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateSupabaseConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: SupabaseConnectionMethodSchema,
credentials: SupabaseConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Supabase).credentials
)
})
]);
export const CreateSupabaseConnectionSchema = ValidateSupabaseConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Supabase)
);
export const UpdateSupabaseConnectionSchema = z
.object({
credentials: SupabaseConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Supabase).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Supabase));
export const SupabaseConnectionListItemSchema = z.object({
name: z.literal("Supabase"),
app: z.literal(AppConnection.Supabase),
methods: z.nativeEnum(SupabaseConnectionMethod).array()
});

View File

@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listProjects as getSupabaseProjects } from "./supabase-connection-fns";
import { TSupabaseConnection } from "./supabase-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TSupabaseConnection>;
export const supabaseConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Supabase, connectionId, actor);
try {
const projects = await getSupabaseProjects(appConnection);
return projects ?? [];
} catch (error) {
logger.error(error, "Failed to establish connection with Supabase");
return [];
}
};
return {
listProjects
};
};

View File

@@ -0,0 +1,44 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateSupabaseConnectionSchema,
SupabaseConnectionSchema,
ValidateSupabaseConnectionCredentialsSchema
} from "./supabase-connection-schemas";
export type TSupabaseConnection = z.infer<typeof SupabaseConnectionSchema>;
export type TSupabaseConnectionInput = z.infer<typeof CreateSupabaseConnectionSchema> & {
app: AppConnection.Supabase;
};
export type TValidateSupabaseConnectionCredentialsSchema = typeof ValidateSupabaseConnectionCredentialsSchema;
export type TSupabaseConnectionConfig = DiscriminativePick<TSupabaseConnection, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TSupabaseProject = {
id: string;
organization_id: string;
name: string;
region: string;
created_at: Date;
status: string;
database: TSupabaseDatabase;
};
type TSupabaseDatabase = {
host: string;
version: string;
postgres_engine: string;
release_channel: string;
};
export type TSupabaseSecret = {
name: string;
value: string;
};

View File

@@ -218,7 +218,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
};
const findWithAssociatedCa = async (
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string },
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string },
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
tx?: Knex
) => {

View File

@@ -1068,11 +1068,11 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate chain" });
const parentCertObj = chainItems[1];
const parentCertSubject = parentCertObj.subject;
const parentSerialNumber = parentCertObj.serialNumber;
const [parentCa] = await certificateAuthorityDAL.findWithAssociatedCa({
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: ca.projectId,
[`${TableName.InternalCertificateAuthority}.dn` as "dn"]: parentCertSubject
[`${TableName.InternalCertificateAuthority}.serialNumber` as "serialNumber"]: parentSerialNumber
});
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({

View File

@@ -30,10 +30,17 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
const removeExpiredTokens = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`);
const BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
const MAX_TTL = 315_360_000; // Maximum TTL value in seconds (10 years)
try {
const docs = (tx || db)(TableName.IdentityAccessToken)
let deletedTokenIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
const getExpiredTokensQuery = (dbClient: Knex | Knex.Transaction) =>
dbClient(TableName.IdentityAccessToken)
.where({
isAccessTokenRevoked: true
})
@@ -47,34 +54,64 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
);
})
.orWhere((qb) => {
void qb.where("accessTokenTTL", ">", 0).andWhere((qb2) => {
void qb2
.where((qb3) => {
void qb3
.whereNotNull("accessTokenLastRenewedAt")
// accessTokenLastRenewedAt + convert_integer_to_seconds(accessTokenTTL) < present_date
.andWhereRaw(
`"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`,
[MAX_TTL]
);
})
.orWhere((qb3) => {
void qb3
.whereNull("accessTokenLastRenewedAt")
// created + convert_integer_to_seconds(accessTokenTTL) < present_date
.andWhereRaw(
`"${TableName.IdentityAccessToken}"."createdAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`,
[MAX_TTL]
);
});
void qb.where("accessTokenTTL", ">", 0).andWhereRaw(
`
-- Check if the token's effective expiration time has passed.
-- The expiration time is calculated by adding its TTL to its last renewal/creation time.
COALESCE(
"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt", -- Use last renewal time if available
"${TableName.IdentityAccessToken}"."createdAt" -- Otherwise, use creation time
)
+ make_interval(
secs => LEAST(
"${TableName.IdentityAccessToken}"."accessTokenTTL", -- Token's specified TTL
? -- Capped by MAX_TTL (parameterized value)
)
)
< NOW() -- Check if the calculated time is before now
`,
[MAX_TTL]
);
});
do {
try {
const deleteBatch = async (dbClient: Knex | Knex.Transaction) => {
const idsToDeleteQuery = getExpiredTokensQuery(dbClient).select("id").limit(BATCH_SIZE);
return dbClient(TableName.IdentityAccessToken).whereIn("id", idsToDeleteQuery).del().returning("id");
};
if (tx) {
// eslint-disable-next-line no-await-in-loop
deletedTokenIds = await deleteBatch(tx);
} else {
// eslint-disable-next-line no-await-in-loop
deletedTokenIds = await db.transaction(async (trx) => {
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
return deleteBatch(trx);
});
})
.delete();
await docs;
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
} catch (error) {
throw new DatabaseError({ error, name: "IdentityAccessTokenPrune" });
}
numberOfRetryOnFailure = 0; // reset
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete a batch of expired identity access tokens on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedTokenIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
logger.error(
`IdentityAccessTokenPrune: Pruning failed and stopped after ${MAX_RETRY_ON_FAILURE} consecutive retries.`
);
}
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
};
return { ...identityAccessTokenOrm, findOne, removeExpiredTokens };

View File

@@ -6,7 +6,8 @@ export type TLoginOciAuthDTO = {
headers: {
authorization: string;
host: string;
"x-date": string;
"x-date"?: string;
date?: string;
};
};

View File

@@ -1280,6 +1280,8 @@ export const orgServiceFactory = ({
message: "No pending invitation found"
});
const organization = await orgDAL.findById(orgId);
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: user.id,
@@ -1302,6 +1304,13 @@ export const orgServiceFactory = ({
return { user };
}
if (
organization.authEnforced &&
!(organization.bypassOrgAuthEnabled && orgMembership.role === OrgMembershipRole.Admin)
) {
return { user };
}
const appCfg = getConfig();
const token = crypto.jwt().sign(
{

View File

@@ -174,6 +174,7 @@ export const fnSecretsV2FromImports = async ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => Promise<string | undefined>;
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => {
@@ -293,7 +294,8 @@ export const fnSecretsV2FromImports = async ({
value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath,
environment: processedImport.environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";

View File

@@ -17,7 +17,7 @@ export const AzureDevOpsSyncDestinationConfigSchema = z.object({
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectId || "Azure DevOps Project ID"),
devopsProjectName: z
.string()
.min(1, "Project name required")
.optional()
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectName || "Azure DevOps Project Name")
});

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const CHECKLY_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Checkly",
destination: SecretSync.Checkly,
connection: AppConnection.Checkly,
canImportSecrets: false
};

View File

@@ -0,0 +1,102 @@
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ChecklyPublicAPI } from "@app/services/app-connection/checkly/checkly-connection-public-client";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { TSecretMap } from "../secret-sync-types";
import { TChecklySyncWithCredentials } from "./checkly-sync-types";
export const ChecklySyncFns = {
async getSecrets(secretSync: TChecklySyncWithCredentials) {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
async syncSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const config = secretSync.destinationConfig;
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
for await (const key of Object.keys(secretMap)) {
try {
const entry = secretMap[key];
// If value is empty, we skip the upsert - checkly does not allow empty values
if (entry.value.trim() === "") {
// Delete the secret from Checkly if its empty
if (!disableSecretDeletion) {
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
key
});
}
continue; // Skip empty values
}
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
key,
value: entry.value,
secret: true,
locked: true
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (disableSecretDeletion) return;
for await (const key of Object.keys(checklySecrets)) {
try {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
if (!secretMap[key]) {
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
key
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
},
async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
const config = secretSync.destinationConfig;
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
for await (const secret of Object.keys(checklySecrets)) {
try {
if (secret in secretMap) {
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
key: secret
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: secret
});
}
}
}
};

View File

@@ -0,0 +1,43 @@
import { z } from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const ChecklySyncDestinationConfigSchema = z.object({
accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
accountName: z.string().min(1, "Account Name is required").max(255, "Account ID must be less than 255 characters")
});
const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const ChecklySyncSchema = BaseSecretSyncSchema(SecretSync.Checkly, ChecklySyncOptionsConfig).extend({
destination: z.literal(SecretSync.Checkly),
destinationConfig: ChecklySyncDestinationConfigSchema
});
export const CreateChecklySyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Checkly,
ChecklySyncOptionsConfig
).extend({
destinationConfig: ChecklySyncDestinationConfigSchema
});
export const UpdateChecklySyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Checkly,
ChecklySyncOptionsConfig
).extend({
destinationConfig: ChecklySyncDestinationConfigSchema.optional()
});
export const ChecklySyncListItemSchema = z.object({
name: z.literal("Checkly"),
connection: z.literal(AppConnection.Checkly),
destination: z.literal(SecretSync.Checkly),
canImportSecrets: z.literal(false)
});

View File

@@ -0,0 +1,23 @@
import z from "zod";
import { TChecklyConnection, TChecklyVariable } from "@app/services/app-connection/checkly";
import { ChecklySyncListItemSchema, ChecklySyncSchema, CreateChecklySyncSchema } from "./checkly-sync-schemas";
export type TChecklySyncListItem = z.infer<typeof ChecklySyncListItemSchema>;
export type TChecklySync = z.infer<typeof ChecklySyncSchema>;
export type TChecklySyncInput = z.infer<typeof CreateChecklySyncSchema>;
export type TChecklySyncWithCredentials = TChecklySync & {
connection: TChecklyConnection;
};
export type TChecklySecret = TChecklyVariable;
export type TChecklyVariablesGraphResponse = {
data: {
variables: Record<string, string>;
};
};

View File

@@ -1,4 +1,6 @@
/* eslint-disable no-await-in-loop */
import { isAxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
@@ -71,7 +73,7 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr
);
};
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: TRenderSecret) => {
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick<TRenderSecret, "key">) => {
const {
destinationConfig,
connection: {
@@ -79,15 +81,24 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s
}
} = secretSync;
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
try {
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
);
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
// If the secret does not exist, we can ignore this error
return;
}
);
throw error;
}
};
const sleep = async () =>
@@ -99,6 +110,11 @@ export const RenderSyncFns = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
for await (const key of Object.keys(secretMap)) {
// If value is empty skip it as render does not allow empty variables
if (secretMap[key].value === "") {
// eslint-disable-next-line no-continue
continue;
}
await putEnvironmentSecret(secretSync, secretMap, key);
await sleep();
}

View File

@@ -22,9 +22,10 @@ export enum SecretSync {
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages",
CloudflareWorkers = "cloudflare-workers",
Supabase = "supabase",
Zabbix = "zabbix",
Railway = "railway"
Railway = "railway",
Checkly = "checkly"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -29,6 +29,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact
import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops";
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants";
import { ChecklySyncFns } from "./checkly/checkly-sync-fns";
import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers";
@@ -44,6 +46,7 @@ import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
import { RailwaySyncFns } from "./railway/railway-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps";
import { SUPABASE_SYNC_LIST_OPTION, SupabaseSyncFns } from "./supabase";
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
@@ -74,9 +77,10 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION,
[SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION,
[SecretSync.Supabase]: SUPABASE_SYNC_LIST_OPTION,
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION,
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION,
[SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -250,6 +254,10 @@ export const SecretSyncFns = {
return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway:
return RailwaySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Checkly:
return ChecklySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -351,6 +359,12 @@ export const SecretSyncFns = {
case SecretSync.Railway:
secretMap = await RailwaySyncFns.getSecrets(secretSync);
break;
case SecretSync.Checkly:
secretMap = await ChecklySyncFns.getSecrets(secretSync);
break;
case SecretSync.Supabase:
secretMap = await SupabaseSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -434,6 +448,10 @@ export const SecretSyncFns = {
return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway:
return RailwaySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Checkly:
return ChecklySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Supabase:
return SupabaseSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -25,9 +25,10 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages",
[SecretSync.CloudflareWorkers]: "Cloudflare Workers",
[SecretSync.Supabase]: "Supabase",
[SecretSync.Zabbix]: "Zabbix",
[SecretSync.Railway]: "Railway"
[SecretSync.Railway]: "Railway",
[SecretSync.Checkly]: "Checkly"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -54,9 +55,10 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Supabase]: AppConnection.Supabase,
[SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway
[SecretSync.Railway]: AppConnection.Railway,
[SecretSync.Checkly]: AppConnection.Checkly
};
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
@@ -83,7 +85,8 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular,
[SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular,
[SecretSync.Supabase]: SecretSyncPlanType.Regular,
[SecretSync.Zabbix]: SecretSyncPlanType.Regular,
[SecretSync.Railway]: SecretSyncPlanType.Regular
[SecretSync.Railway]: SecretSyncPlanType.Regular,
[SecretSync.Checkly]: SecretSyncPlanType.Regular
};

View File

@@ -231,7 +231,8 @@ export const secretSyncQueueFactory = ({
environment: environment.slug,
secretPath: folder.path,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
secretMap[secretKey] = { value: expandedSecretValue || "" };

View File

@@ -72,6 +72,12 @@ import {
TAzureKeyVaultSyncListItem,
TAzureKeyVaultSyncWithCredentials
} from "./azure-key-vault";
import {
TChecklySync,
TChecklySyncInput,
TChecklySyncListItem,
TChecklySyncWithCredentials
} from "./checkly/checkly-sync-types";
import {
TCloudflarePagesSync,
TCloudflarePagesSyncInput,
@@ -112,6 +118,12 @@ import {
TRenderSyncListItem,
TRenderSyncWithCredentials
} from "./render/render-sync-types";
import {
TSupabaseSync,
TSupabaseSyncInput,
TSupabaseSyncListItem,
TSupabaseSyncWithCredentials
} from "./supabase/supabase-sync-types";
import {
TTeamCitySync,
TTeamCitySyncInput,
@@ -152,7 +164,9 @@ export type TSecretSync =
| TCloudflarePagesSync
| TCloudflareWorkersSync
| TZabbixSync
| TRailwaySync;
| TRailwaySync
| TChecklySync
| TSupabaseSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -179,7 +193,9 @@ export type TSecretSyncWithCredentials =
| TCloudflarePagesSyncWithCredentials
| TCloudflareWorkersSyncWithCredentials
| TZabbixSyncWithCredentials
| TRailwaySyncWithCredentials;
| TRailwaySyncWithCredentials
| TChecklySyncWithCredentials
| TSupabaseSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -206,7 +222,9 @@ export type TSecretSyncInput =
| TCloudflarePagesSyncInput
| TCloudflareWorkersSyncInput
| TZabbixSyncInput
| TRailwaySyncInput;
| TRailwaySyncInput
| TChecklySyncInput
| TSupabaseSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -233,7 +251,9 @@ export type TSecretSyncListItem =
| TCloudflarePagesSyncListItem
| TCloudflareWorkersSyncListItem
| TZabbixSyncListItem
| TRailwaySyncListItem;
| TRailwaySyncListItem
| TChecklySyncListItem
| TSupabaseSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

@@ -0,0 +1,4 @@
export * from "./supabase-sync-constants";
export * from "./supabase-sync-fns";
export * from "./supabase-sync-schemas";
export * from "./supabase-sync-types";

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const SUPABASE_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Supabase",
destination: SecretSync.Supabase,
connection: AppConnection.Supabase,
canImportSecrets: false
};

View File

@@ -0,0 +1,102 @@
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { chunkArray } from "@app/lib/fn";
import { TSupabaseSecret } from "@app/services/app-connection/supabase";
import { SupabasePublicAPI } from "@app/services/app-connection/supabase/supabase-connection-public-client";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { TSecretMap } from "../secret-sync-types";
import { TSupabaseSyncWithCredentials } from "./supabase-sync-types";
const SUPABASE_INTERNAL_SECRETS = ["SUPABASE_URL", "SUPABASE_ANON_KEY", "SUPABASE_SERVICE_ROLE_KEY", "SUPABASE_DB_URL"];
export const SupabaseSyncFns = {
async getSecrets(secretSync: TSupabaseSyncWithCredentials) {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
async syncSecrets(secretSync: TSupabaseSyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const config = secretSync.destinationConfig;
const variables = await SupabasePublicAPI.getVariables(secretSync.connection, config.projectId);
const supabaseSecrets = new Map(variables!.map((variable) => [variable.name, variable]));
const toCreate: TSupabaseSecret[] = [];
for (const key of Object.keys(secretMap)) {
const variable: TSupabaseSecret = { name: key, value: secretMap[key].value ?? "" };
toCreate.push(variable);
}
for await (const batch of chunkArray(toCreate, 100)) {
try {
await SupabasePublicAPI.createVariables(secretSync.connection, config.projectId, ...batch);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: batch[0].name // Use the first key in the batch for error reporting
});
}
}
if (disableSecretDeletion) return;
const toDelete: string[] = [];
for (const key of supabaseSecrets.keys()) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", keySchema) || SUPABASE_INTERNAL_SECRETS.includes(key)) continue;
if (!secretMap[key]) {
toDelete.push(key);
}
}
for await (const batch of chunkArray(toDelete, 100)) {
try {
await SupabasePublicAPI.deleteVariables(secretSync.connection, config.projectId, ...batch);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: batch[0] // Use the first key in the batch for error reporting
});
}
}
},
async removeSecrets(secretSync: TSupabaseSyncWithCredentials, secretMap: TSecretMap) {
const config = secretSync.destinationConfig;
const variables = await SupabasePublicAPI.getVariables(secretSync.connection, config.projectId);
const supabaseSecrets = new Map(variables!.map((variable) => [variable.name, variable]));
const toDelete: string[] = [];
for (const key of supabaseSecrets.keys()) {
if (SUPABASE_INTERNAL_SECRETS.includes(key) || !(key in secretMap)) continue;
toDelete.push(key);
}
for await (const batch of chunkArray(toDelete, 100)) {
try {
await SupabasePublicAPI.deleteVariables(secretSync.connection, config.projectId, ...batch);
} catch (error) {
throw new SecretSyncError({
error,
secretKey: batch[0] // Use the first key in the batch for error reporting
});
}
}
}
};

View File

@@ -0,0 +1,43 @@
import { z } from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const SupabaseSyncDestinationConfigSchema = z.object({
projectId: z.string().max(255).min(1, "Project ID is required"),
projectName: z.string().max(255).min(1, "Project Name is required")
});
const SupabaseSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const SupabaseSyncSchema = BaseSecretSyncSchema(SecretSync.Supabase, SupabaseSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Supabase),
destinationConfig: SupabaseSyncDestinationConfigSchema
});
export const CreateSupabaseSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Supabase,
SupabaseSyncOptionsConfig
).extend({
destinationConfig: SupabaseSyncDestinationConfigSchema
});
export const UpdateSupabaseSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Supabase,
SupabaseSyncOptionsConfig
).extend({
destinationConfig: SupabaseSyncDestinationConfigSchema.optional()
});
export const SupabaseSyncListItemSchema = z.object({
name: z.literal("Supabase"),
connection: z.literal(AppConnection.Supabase),
destination: z.literal(SecretSync.Supabase),
canImportSecrets: z.literal(false)
});

View File

@@ -0,0 +1,21 @@
import z from "zod";
import { TSupabaseConnection } from "@app/services/app-connection/supabase";
import { CreateSupabaseSyncSchema, SupabaseSyncListItemSchema, SupabaseSyncSchema } from "./supabase-sync-schemas";
export type TSupabaseSyncListItem = z.infer<typeof SupabaseSyncListItemSchema>;
export type TSupabaseSync = z.infer<typeof SupabaseSyncSchema>;
export type TSupabaseSyncInput = z.infer<typeof CreateSupabaseSyncSchema>;
export type TSupabaseSyncWithCredentials = TSupabaseSync & {
connection: TSupabaseConnection;
};
export type TSupabaseVariablesGraphResponse = {
data: {
variables: Record<string, string>;
};
};

View File

@@ -614,6 +614,7 @@ export const expandSecretReferencesFactory = ({
secretPath: string;
environment: string;
shouldStackTrace?: boolean;
secretKey: string;
}) => {
const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode;
@@ -656,7 +657,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretKey}' in environment '${environment}' at path '${secretPath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(environment, secretPath);
@@ -675,7 +676,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretReferenceKey}' in environment '${secretReferenceEnvironment}' at path '${secretReferencePath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@@ -692,6 +693,7 @@ export const expandSecretReferencesFactory = ({
secretPath: referencedSecretPath,
environment: referencedSecretEnvironmentSlug,
depth: depth + 1,
secretKey: referencedSecretKey,
trace
};
@@ -726,6 +728,7 @@ export const expandSecretReferencesFactory = ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
@@ -741,6 +744,7 @@ export const expandSecretReferencesFactory = ({
value?: string;
secretPath: string;
environment: string;
secretKey: string;
}) => {
const { stackTrace, expandedValue } = await recursivelyExpandSecret({ ...inputSecret, shouldStackTrace: true });
return { stackTrace, expandedValue };

View File

@@ -1105,7 +1105,7 @@ export const secretV2BridgeServiceFactory = ({
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
await Promise.allSettled(
const settledPromises = await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
@@ -1113,7 +1113,8 @@ export const secretV2BridgeServiceFactory = ({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
@@ -1121,6 +1122,35 @@ export const secretV2BridgeServiceFactory = ({
)
)
);
const errors: { path: string; error: string }[] = [];
settledPromises.forEach((outerResult: PromiseSettledResult<PromiseSettledResult<void>[]>, outerIndex) => {
const groupedPath = Object.keys(secretsGroupByPath)[outerIndex];
if (outerResult.status === "rejected") {
errors.push({
path: groupedPath,
error: `Failed to process secret group: ${outerResult.reason}`
});
} else {
// Check inner promise results
outerResult.value.forEach((innerResult: PromiseSettledResult<void>) => {
if (innerResult.status === "rejected") {
const reason = innerResult.reason as ForbiddenRequestError;
errors.push({
path: groupedPath,
error: reason.message
});
}
});
}
});
if (errors.length > 0) {
throw new ForbiddenRequestError({
message: "Failed to expand one or more secret references",
details: errors.map((err) => err.error)
});
}
}
if (!includeImports) {
@@ -1424,7 +1454,8 @@ export const secretV2BridgeServiceFactory = ({
environment,
secretPath: path,
value: secretValue,
skipMultilineEncoding: secret.skipMultilineEncoding
skipMultilineEncoding: secret.skipMultilineEncoding,
secretKey: secret.key
});
secretValue = expandedSecretValue || "";
@@ -2722,7 +2753,8 @@ export const secretV2BridgeServiceFactory = ({
const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({
environment,
secretPath,
value: decryptedSecretValue
value: decryptedSecretValue,
secretKey: secretName
});
return { tree: stackTrace, value: expandedValue };

View File

@@ -426,7 +426,8 @@ export const secretQueueFactory = ({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
content[secretKey] = { value: expandedSecretValue || "" };

View File

@@ -20,7 +20,7 @@ func CheckForUpdate() {
if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" {
return
}
latestVersion, _, err := getLatestTag("Infisical", "infisical")
latestVersion, _, err := getLatestTag("Infisical", "cli")
if err != nil {
log.Debug().Err(err)
// do nothing and continue
@@ -98,7 +98,7 @@ func getLatestTag(repoOwner string, repoName string) (string, string, error) {
return "", "", fmt.Errorf("failed to unmarshal github response: %w", err)
}
tag_prefix := "infisical-cli/v"
tag_prefix := "v"
// Extract the version from the first valid tag
version := strings.TrimPrefix(releaseDetails.TagName, tag_prefix)

View File

@@ -33,6 +33,7 @@ Every feature/problem is unique, but your design docs should generally include t
- A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs).
3. **Context**
- Explain the problem's background, why it's important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.).
- Consider whether this feature has notable sales implications (e.g., affects pricing, customer commitments, go-to-market strategy, or competitive positioning) that would require Sales team input and approval.
4. **Solution**
- Provide a big-picture explanation of the solution, followed by detailed technical architecture.
@@ -76,3 +77,11 @@ Before sharing your design docs with others, review your design doc as if you we
- Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up.
4. **Team Review and Feedback**
- Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on.
5. **Sales Approval (When Applicable)**
- If your design document has notable sales implications, get explicit approval from the Sales team before proceeding to implementation. This includes features that:
- Affect pricing models or billing structures
- Impact customer commitments or contractual obligations
- Change core product functionality that's actively being sold
- Introduce new capabilities that could affect competitive positioning
- Modify user experience in ways that could impact customer acquisition or retention
- Share the design document with the Sales team to ensure alignment between the proposed technical approach and sales strategy, pricing models, and market positioning.

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/checkly/available"
---

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/checkly"
---
<Note>
Check out the configuration docs for [Checkly Connections](/integrations/app-connections/checkly) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/checkly/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/checkly/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/checkly/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/checkly"
---

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/checkly/{connectionId}"
---
<Note>
Check out the configuration docs for [Checkly Connections](/integrations/app-connections/checkly) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/supabase/available"
---

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/supabase"
---
<Note>
Check out the configuration docs for [Supabase Connections](/integrations/app-connections/supabase) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/supabase/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/supabase/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/supabase/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/supabase"
---

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/supabase/{connectionId}"
---
<Note>
Check out the configuration docs for [Supabase Connections](/integrations/app-connections/supabase) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/checkly"
---

Some files were not shown because too many files have changed in this diff Show More