mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 00:29:04 +00:00
Merge branch 'main' into feature/north-flank-app-connection
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ COPY --from=build /app .
|
||||
# Install Infisical CLI
|
||||
RUN apt-get install -y curl bash && \
|
||||
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && apt-get install -y infisical=0.41.89 git
|
||||
apt-get update && apt-get install -y infisical=0.43.14 git
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
@@ -54,7 +54,7 @@ RUN apt-get install -y opensc
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.41.89
|
||||
apt-get install -y infisical=0.43.14
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.41.89
|
||||
apt-get install -y infisical=0.43.14
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { AccessScope, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGroupsTable = await knex.schema.hasTable(TableName.Groups);
|
||||
const hasMembershipTable = await knex.schema.hasTable(TableName.Membership);
|
||||
const hasMembershipRoleTable = await knex.schema.hasTable(TableName.MembershipRole);
|
||||
|
||||
if (!hasGroupsTable || !hasMembershipTable || !hasMembershipRoleTable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupsWithoutMembership = await knex
|
||||
.select(
|
||||
`${TableName.Groups}.id`,
|
||||
`${TableName.Groups}.orgId`,
|
||||
`${TableName.Groups}.role`,
|
||||
`${TableName.Groups}.roleId`
|
||||
)
|
||||
.from(TableName.Groups)
|
||||
.leftJoin(TableName.Membership, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`)
|
||||
.whereNull(`${TableName.Membership}.actorGroupId`);
|
||||
|
||||
if (groupsWithoutMembership.length > 0) {
|
||||
const membershipInserts = groupsWithoutMembership.map((group) => ({
|
||||
actorGroupId: group.id,
|
||||
scope: AccessScope.Organization,
|
||||
scopeOrgId: group.orgId,
|
||||
isActive: true
|
||||
}));
|
||||
|
||||
const insertedMemberships = await knex(TableName.Membership).insert(membershipInserts).returning("*");
|
||||
|
||||
const membershipRoleInserts = insertedMemberships.map((membership, index) => {
|
||||
const group = groupsWithoutMembership[index];
|
||||
return {
|
||||
membershipId: membership.id,
|
||||
role: group.role,
|
||||
customRoleId: group.roleId
|
||||
};
|
||||
});
|
||||
|
||||
await knex(TableName.MembershipRole).insert(membershipRoleInserts);
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.Membership, (t) => {
|
||||
t.check(
|
||||
`("actorUserId" IS NOT NULL OR "actorIdentityId" IS NOT NULL OR "actorGroupId" IS NOT NULL)`,
|
||||
undefined,
|
||||
"at_least_one_actor"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.Membership, (t) => {
|
||||
t.dropChecks("at_least_one_actor");
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CreateMySQLAccountSchema,
|
||||
SanitizedMySQLAccountWithResourceSchema,
|
||||
UpdateMySQLAccountSchema
|
||||
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import {
|
||||
CreatePostgresAccountSchema,
|
||||
@@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
||||
createAccountSchema: CreatePostgresAccountSchema,
|
||||
updateAccountSchema: UpdatePostgresAccountSchema
|
||||
});
|
||||
},
|
||||
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.MySQL,
|
||||
accountResponseSchema: SanitizedMySQLAccountWithResourceSchema,
|
||||
createAccountSchema: CreateMySQLAccountSchema,
|
||||
updateAccountSchema: UpdateMySQLAccountSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { PamFoldersSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@@ -10,8 +11,10 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// Use z.union([...]) when more resources are added
|
||||
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema;
|
||||
const SanitizedAccountSchema = z.union([
|
||||
SanitizedPostgresAccountWithResourceSchema,
|
||||
SanitizedMySQLAccountWithResourceSchema
|
||||
]);
|
||||
|
||||
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
|
||||
@@ -4,6 +4,11 @@ import {
|
||||
SanitizedPostgresResourceSchema,
|
||||
UpdatePostgresResourceSchema
|
||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
import {
|
||||
CreateMySQLResourceSchema,
|
||||
MySQLResourceSchema,
|
||||
UpdateMySQLResourceSchema
|
||||
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
|
||||
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
||||
|
||||
@@ -16,5 +21,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
||||
createResourceSchema: CreatePostgresResourceSchema,
|
||||
updateResourceSchema: UpdatePostgresResourceSchema
|
||||
});
|
||||
},
|
||||
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.MySQL,
|
||||
resourceResponseSchema: MySQLResourceSchema,
|
||||
createResourceSchema: CreateMySQLResourceSchema,
|
||||
updateResourceSchema: UpdateMySQLResourceSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
MySQLResourceListItemSchema,
|
||||
SanitizedMySQLResourceSchema
|
||||
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import {
|
||||
PostgresResourceListItemSchema,
|
||||
SanitizedPostgresResourceSchema
|
||||
@@ -9,10 +13,12 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// Use z.union([...]) when more resources are added
|
||||
const SanitizedResourceSchema = SanitizedPostgresResourceSchema;
|
||||
const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
|
||||
|
||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
|
||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||
PostgresResourceListItemSchema,
|
||||
MySQLResourceListItemSchema
|
||||
]);
|
||||
|
||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
|
||||
@@ -2,14 +2,14 @@ import { z } from "zod";
|
||||
|
||||
import { PamSessionsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// Use z.union([]) once there's multiple
|
||||
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
|
||||
const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
|
||||
|
||||
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||
// Meant to be hit solely by gateway identities
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
TOrgPlanDTO,
|
||||
TOrgPlansTableDTO,
|
||||
TOrgPmtMethodsDTO,
|
||||
TPlanBillingInfo,
|
||||
TStartOrgTrialDTO,
|
||||
TUpdateOrgBillingDetailsDTO
|
||||
} from "./license-types";
|
||||
@@ -465,6 +466,21 @@ export const licenseServiceFactory = ({
|
||||
return { url };
|
||||
};
|
||||
|
||||
const getUsageMetrics = async (orgId: string) => {
|
||||
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
|
||||
orgDAL.countAllOrgMembers(orgId),
|
||||
licenseDAL.countOfOrgIdentities(orgId),
|
||||
projectDAL.countOfOrgProjects(orgId)
|
||||
]);
|
||||
|
||||
return {
|
||||
orgMembersUsed,
|
||||
identityUsed,
|
||||
projectCount,
|
||||
totalIdentities: identityUsed + orgMembersUsed
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
@@ -483,10 +499,16 @@ export const licenseServiceFactory = ({
|
||||
});
|
||||
}
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
const { data } = await licenseServerCloudApi.request.get<TPlanBillingInfo>(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
||||
);
|
||||
return data;
|
||||
const { identityUsed, orgMembersUsed } = await getUsageMetrics(orgId);
|
||||
|
||||
return {
|
||||
...data,
|
||||
users: orgMembersUsed,
|
||||
identities: identityUsed
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -495,7 +517,9 @@ export const licenseServiceFactory = ({
|
||||
interval: "month",
|
||||
intervalCount: 1,
|
||||
amount: 0,
|
||||
quantity: 1
|
||||
quantity: 1,
|
||||
users: 0,
|
||||
identities: 0
|
||||
};
|
||||
};
|
||||
|
||||
@@ -539,21 +563,6 @@ export const licenseServiceFactory = ({
|
||||
throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`);
|
||||
};
|
||||
|
||||
const getUsageMetrics = async (orgId: string) => {
|
||||
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
|
||||
orgDAL.countAllOrgMembers(orgId),
|
||||
licenseDAL.countOfOrgIdentities(orgId),
|
||||
projectDAL.countOfOrgProjects(orgId)
|
||||
]);
|
||||
|
||||
return {
|
||||
orgMembersUsed,
|
||||
identityUsed,
|
||||
projectCount,
|
||||
totalIdentities: identityUsed + orgMembersUsed
|
||||
};
|
||||
};
|
||||
|
||||
// returns org current plan feature table
|
||||
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
|
||||
@@ -22,6 +22,15 @@ export type TOfflineLicense = {
|
||||
features: TFeatureSet;
|
||||
};
|
||||
|
||||
export type TPlanBillingInfo = {
|
||||
currentPeriodStart: number;
|
||||
currentPeriodEnd: number;
|
||||
interval: "month" | "year";
|
||||
intervalCount: number;
|
||||
amount: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export type TFeatureSet = {
|
||||
_id: null;
|
||||
slug: string | null;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { MySQLResourceListItemSchema } from "./mysql-resource-schemas";
|
||||
|
||||
export const getMySQLResourceListItem = () => {
|
||||
return {
|
||||
name: MySQLResourceListItemSchema.shape.name.value,
|
||||
resource: MySQLResourceListItemSchema.shape.resource.value
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import {
|
||||
BaseSqlAccountCredentialsSchema,
|
||||
BaseSqlResourceConnectionDetailsSchema
|
||||
} from "../shared/sql/sql-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export const MySQLResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema.extend({
|
||||
// MySQL db in many cases the db will not be provided when making connection
|
||||
database: z.string().trim()
|
||||
});
|
||||
export const MySQLAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
|
||||
|
||||
const BaseMySQLResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.MySQL) });
|
||||
|
||||
export const MySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const SanitizedMySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.pick({
|
||||
username: true
|
||||
})
|
||||
.nullable()
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const MySQLResourceListItemSchema = z.object({
|
||||
name: z.literal("MySQL"),
|
||||
resource: z.literal(PamResource.MySQL)
|
||||
});
|
||||
|
||||
export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema.optional(),
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
// Accounts
|
||||
export const MySQLAccountSchema = BasePamAccountSchema.extend({
|
||||
credentials: MySQLAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const CreateMySQLAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||
credentials: MySQLAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateMySQLAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||
credentials: MySQLAccountCredentialsSchema.optional()
|
||||
});
|
||||
|
||||
export const SanitizedMySQLAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||
credentials: MySQLAccountCredentialsSchema.pick({
|
||||
username: true
|
||||
})
|
||||
});
|
||||
|
||||
// Sessions
|
||||
export const MySQLSessionCredentialsSchema = MySQLResourceConnectionDetailsSchema.and(MySQLAccountCredentialsSchema);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
MySQLAccountCredentialsSchema,
|
||||
MySQLAccountSchema,
|
||||
MySQLResourceConnectionDetailsSchema,
|
||||
MySQLResourceSchema
|
||||
} from "./mysql-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export type TMySQLResource = z.infer<typeof MySQLResourceSchema>;
|
||||
export type TMySQLResourceConnectionDetails = z.infer<typeof MySQLResourceConnectionDetailsSchema>;
|
||||
|
||||
// Accounts
|
||||
export type TMySQLAccount = z.infer<typeof MySQLAccountSchema>;
|
||||
export type TMySQLAccountCredentials = z.infer<typeof MySQLAccountCredentialsSchema>;
|
||||
@@ -1,3 +1,4 @@
|
||||
export enum PamResource {
|
||||
Postgres = "postgres"
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql"
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
||||
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
||||
|
||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation
|
||||
};
|
||||
|
||||
@@ -3,11 +3,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
|
||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||
|
||||
export const listResourceOptions = () => {
|
||||
return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
// Resource
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import {
|
||||
TMySQLAccount,
|
||||
TMySQLAccountCredentials,
|
||||
TMySQLResource,
|
||||
TMySQLResourceConnectionDetails
|
||||
} from "./mysql/mysql-resource-types";
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import {
|
||||
TPostgresAccount,
|
||||
@@ -8,12 +14,13 @@ import {
|
||||
} from "./postgres/postgres-resource-types";
|
||||
|
||||
// Resource types
|
||||
export type TPamResource = TPostgresResource;
|
||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
||||
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||
|
||||
// Account types
|
||||
export type TPamAccount = TPostgresAccount;
|
||||
export type TPamAccountCredentials = TPostgresAccountCredentials;
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||
|
||||
// Resource DTOs
|
||||
export type TCreateResourceDTO = Pick<
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import knex, { Knex } from "knex";
|
||||
import knex from "knex";
|
||||
import mysql, { Connection } from "mysql2/promise";
|
||||
import * as pg from "pg";
|
||||
import tls, { PeerCertificate } from "tls";
|
||||
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
@@ -20,30 +22,160 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
|
||||
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
||||
const SIMPLE_QUERY = "select 1";
|
||||
|
||||
const SQL_CONNECTION_CLIENT_MAP = {
|
||||
[PamResource.Postgres]: "pg"
|
||||
};
|
||||
export interface SqlResourceConnection {
|
||||
/**
|
||||
* Check and see if the connection is good or not.
|
||||
*
|
||||
* @param connectOnly when true, if we only want to know that making the connection is possible or not,
|
||||
* we don't care about authentication failures
|
||||
* @returns Promise to be resolved when the connection is good, otherwise an error will be errbacked
|
||||
*/
|
||||
validate: (connectOnly: boolean) => Promise<void>;
|
||||
|
||||
const getConnectionConfig = (
|
||||
resourceType: PamResource,
|
||||
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails
|
||||
) => {
|
||||
switch (resourceType) {
|
||||
/**
|
||||
* Rotate password and return the new credentials.
|
||||
*
|
||||
* @param currentCredentials the current credentials to rotate
|
||||
*
|
||||
* @returns Promise to be resolved with the new credentials
|
||||
*/
|
||||
rotateCredentials: (currentCredentials: TSqlAccountCredentials) => Promise<TSqlAccountCredentials>;
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
*
|
||||
* @returns Promise for closing the connection
|
||||
*/
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
const makeSqlConnection = (
|
||||
proxyPort: number,
|
||||
config: {
|
||||
connectionDetails: TSqlResourceConnectionDetails;
|
||||
resourceType: PamResource;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
): SqlResourceConnection => {
|
||||
const { connectionDetails, resourceType, username, password } = config;
|
||||
const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails;
|
||||
const actualUsername = username ?? TEST_CONNECTION_USERNAME; // Use provided username or fallback
|
||||
const actualPassword = password ?? TEST_CONNECTION_PASSWORD; // Use provided password or fallback
|
||||
switch (config.resourceType) {
|
||||
case PamResource.Postgres: {
|
||||
const client = knex({
|
||||
client: "pg",
|
||||
connection: {
|
||||
host: "localhost",
|
||||
port: proxyPort,
|
||||
user: actualUsername,
|
||||
password: actualPassword,
|
||||
database: connectionDetails.database,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate,
|
||||
servername: host,
|
||||
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
||||
// but validate the certificate against the actual hostname
|
||||
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
||||
return tls.checkServerIdentity(host, cert);
|
||||
}
|
||||
}
|
||||
: false
|
||||
}
|
||||
});
|
||||
return {
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate,
|
||||
servername: host,
|
||||
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
||||
// but validate the certificate against the actual hostname
|
||||
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
||||
return tls.checkServerIdentity(host, cert);
|
||||
validate: async (connectOnly) => {
|
||||
try {
|
||||
await client.raw(SIMPLE_QUERY);
|
||||
} catch (error) {
|
||||
if (error instanceof pg.DatabaseError) {
|
||||
// Hacky way to know if we successfully hit the database.
|
||||
// TODO: potentially two approaches to solve the problem.
|
||||
// 1. change the work flow, add account first then resource
|
||||
// 2. modify relay to add a new endpoint for returning if the target host is healthy or not
|
||||
// (like being able to do an auth handshake regardless pass or not)
|
||||
if (
|
||||
connectOnly &&
|
||||
(error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ||
|
||||
error.message.includes("no pg_hba.conf entry for host"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
: false
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
},
|
||||
rotateCredentials: async (currentCredentials) => {
|
||||
const newPassword = alphaNumericNanoId(32);
|
||||
// Note: The generated random password is not really going to make SQL Injection possible.
|
||||
// The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL,
|
||||
// parameters binding is not supported. But just in case if the this code got copied
|
||||
// around and repurposed, let's just do some naive escaping regardless
|
||||
await client.raw(`ALTER USER :username: WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`, {
|
||||
username: currentCredentials.username
|
||||
});
|
||||
return { username: currentCredentials.username, password: newPassword };
|
||||
},
|
||||
close: () => client.destroy()
|
||||
};
|
||||
}
|
||||
case PamResource.MySQL: {
|
||||
return {
|
||||
validate: async (connectOnly) => {
|
||||
let client: Connection | null = null;
|
||||
try {
|
||||
// Notice: the reason we are not using Knex for mysql2 is because we don't need any fancy feature from Knex.
|
||||
// mysql2 doesn't provide custom ssl verification function pass in.
|
||||
// ref: https://github.com/sidorares/node-mysql2/blob/2543272a2ada8d8a07f74582549d7dd3fe948e2d/lib/base/connection.js#L358-L362
|
||||
// and then even I tried to workaround it with Knex's pool afterCreate hook, but then encounter a bug:
|
||||
// ref: https://github.com/knex/knex/issues/5352
|
||||
// It appears that using Knex causing more troubles than not, we are just checking the connections,
|
||||
// so it's much easier to create raw connection with the driver lib directly
|
||||
client = await mysql.createConnection({
|
||||
host: "localhost",
|
||||
port: proxyPort,
|
||||
user: actualUsername, // Use provided username or fallback
|
||||
password: actualPassword, // Use provided password or fallback
|
||||
database: connectionDetails.database,
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
await client.query(SIMPLE_QUERY);
|
||||
} catch (error) {
|
||||
if (connectOnly) {
|
||||
// Hacky way to know if we successfully hit the database.
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.startsWith(`Access denied for user '${TEST_CONNECTION_USERNAME}'@`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// TODO: handle other errors, and throw standardlized errors providing user-friendly msg
|
||||
throw error;
|
||||
} finally {
|
||||
await client?.end();
|
||||
}
|
||||
},
|
||||
rotateCredentials: async () => {
|
||||
// TODO: the pwd rotation for MySQL is not supported yet
|
||||
throw new BadRequestError({
|
||||
message: "Unsupported operation"
|
||||
});
|
||||
},
|
||||
close: async () => {}
|
||||
};
|
||||
}
|
||||
default:
|
||||
@@ -62,10 +194,9 @@ export const executeWithGateway = async <T>(
|
||||
password?: string;
|
||||
},
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||
operation: (client: Knex) => Promise<T>
|
||||
operation: (connection: SqlResourceConnection) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const { connectionDetails, resourceType, gatewayId, username, password } = config;
|
||||
|
||||
const { connectionDetails, gatewayId } = config;
|
||||
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
||||
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId,
|
||||
@@ -79,22 +210,11 @@ export const executeWithGateway = async <T>(
|
||||
|
||||
return withGatewayV2Proxy(
|
||||
async (proxyPort) => {
|
||||
const client = knex({
|
||||
client: SQL_CONNECTION_CLIENT_MAP[resourceType],
|
||||
connection: {
|
||||
database: connectionDetails.database,
|
||||
port: proxyPort,
|
||||
host: "localhost",
|
||||
user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback
|
||||
password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
...getConnectionConfig(resourceType, connectionDetails)
|
||||
}
|
||||
});
|
||||
const connection = makeSqlConnection(proxyPort, config);
|
||||
try {
|
||||
return await operation(client);
|
||||
return await operation(connection);
|
||||
} finally {
|
||||
await client.destroy();
|
||||
await connection.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -115,25 +235,14 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
const validateConnection = async () => {
|
||||
try {
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
||||
await client.raw("Select 1");
|
||||
await client.validate(true);
|
||||
});
|
||||
return connectionDetails;
|
||||
} catch (error) {
|
||||
// Hacky way to know if we successfully hit the database
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) {
|
||||
return connectionDetails;
|
||||
}
|
||||
|
||||
if (error.message.includes("no pg_hba.conf entry for host")) {
|
||||
return connectionDetails;
|
||||
}
|
||||
|
||||
if (error.message === "Connection terminated unexpectedly") {
|
||||
throw new BadRequestError({
|
||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||
});
|
||||
}
|
||||
if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") {
|
||||
throw new BadRequestError({
|
||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
@@ -156,11 +265,12 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
},
|
||||
gatewayV2Service,
|
||||
async (client) => {
|
||||
await client.raw("Select 1");
|
||||
await client.validate(false);
|
||||
}
|
||||
);
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
// TODO: extract these logic into each SQL connection
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
||||
throw new BadRequestError({
|
||||
@@ -186,9 +296,7 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
currentCredentials
|
||||
) => {
|
||||
try {
|
||||
const newPassword = alphaNumericNanoId(32);
|
||||
|
||||
await executeWithGateway(
|
||||
return await executeWithGateway(
|
||||
{
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
@@ -197,20 +305,8 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
password: rotationAccountCredentials.password
|
||||
},
|
||||
gatewayV2Service,
|
||||
async (client) => {
|
||||
switch (resourceType) {
|
||||
case PamResource.Postgres:
|
||||
await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]);
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Password rotation for ${resourceType as PamResource} is not supported.`
|
||||
});
|
||||
}
|
||||
}
|
||||
(client) => client.rotateCredentials(currentCredentials)
|
||||
);
|
||||
|
||||
return { username: currentCredentials.username, password: newPassword };
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { TMySQLAccountCredentials, TMySQLResourceConnectionDetails } from "../../mysql/mysql-resource-types";
|
||||
import {
|
||||
TPostgresAccountCredentials,
|
||||
TPostgresResourceConnectionDetails
|
||||
} from "../../postgres/postgres-resource-types";
|
||||
|
||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
||||
export type TSqlAccountCredentials = TPostgresAccountCredentials;
|
||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||
export type TSqlAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||
|
||||
@@ -84,7 +84,7 @@ type TSamlConfigServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||
membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
|
||||
membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find" | "create">;
|
||||
};
|
||||
|
||||
export const samlConfigServiceFactory = ({
|
||||
@@ -183,6 +183,22 @@ export const samlConfigServiceFactory = ({
|
||||
transaction
|
||||
);
|
||||
orgGroupsMap.set(groupName, newGroup);
|
||||
const orgMembership = await membershipGroupDAL.create(
|
||||
{
|
||||
actorGroupId: newGroup.id,
|
||||
scope: AccessScope.Organization,
|
||||
scopeOrgId: orgId
|
||||
},
|
||||
transaction
|
||||
);
|
||||
await membershipRoleDAL.create(
|
||||
{
|
||||
membershipId: orgMembership.id,
|
||||
role: OrgMembershipRole.NoAccess,
|
||||
customRoleId: null
|
||||
},
|
||||
transaction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import { z } from "zod";
|
||||
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { QueueWorkerProfile } from "@app/lib/types";
|
||||
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { removeTrailingSlash } from "../fn";
|
||||
import { CustomLogger } from "../logger/logger";
|
||||
import { zpStr } from "../zod";
|
||||
import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
|
||||
export const GITLAB_URL = "https://gitlab.com";
|
||||
|
||||
|
||||
@@ -43,6 +43,6 @@ export const GenericResourceNameSchema = z
|
||||
export const BaseSecretNameSchema = z.string().trim().min(1);
|
||||
|
||||
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||
(el) => !el.includes(":"),
|
||||
"Secret name cannot contain colon."
|
||||
).refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
|
||||
(el) => !el.includes(":") && !el.includes("/"),
|
||||
"Secret name cannot contain colon or forward slash."
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ interface PrincipalArnEntity {
|
||||
SessionInfo: string; // Only populated for assumed-role
|
||||
}
|
||||
|
||||
export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => {
|
||||
export const extractPrincipalArnEntity = (arn: string, formatAsIamRole: boolean = false): PrincipalArnEntity => {
|
||||
// split the ARN into parts using ":" as the delimiter
|
||||
const fullParts = arn.split(":");
|
||||
if (fullParts.length !== 6) {
|
||||
@@ -49,7 +49,7 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => {
|
||||
}
|
||||
// assumed roles use a special format where the friendly name is the role name
|
||||
const [roleName, sessionId] = rest;
|
||||
finalType = "assumed-role";
|
||||
finalType = formatAsIamRole ? "role" : "assumed-role";
|
||||
friendlyName = roleName;
|
||||
sessionInfo = sessionId;
|
||||
break;
|
||||
@@ -83,9 +83,11 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => {
|
||||
* Extracts the identity ARN from the GetCallerIdentity response to one of the following formats:
|
||||
* - arn:aws:iam::123456789012:user/MyUserName
|
||||
* - arn:aws:iam::123456789012:role/MyRoleName
|
||||
* - arn:aws-us-gov:iam::123456789012:user/MyUserName (GovCloud)
|
||||
* - arn:aws-us-gov:iam::123456789012:role/MyRoleName (GovCloud)
|
||||
*/
|
||||
export const extractPrincipalArn = (arn: string) => {
|
||||
const entity = extractPrincipalArnEntity(arn);
|
||||
export const extractPrincipalArn = (arn: string, formatAsIamRole: boolean = false) => {
|
||||
const entity = extractPrincipalArnEntity(arn, formatAsIamRole);
|
||||
|
||||
return `arn:aws:${entity.Service}::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`;
|
||||
return `arn:${entity.Partition}:${formatAsIamRole ? "iam" : entity.Service}::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`;
|
||||
};
|
||||
|
||||
@@ -158,7 +158,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
// considers exact matches + wildcard matches
|
||||
// heavily validated in router
|
||||
const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`);
|
||||
return regex.test(formattedArn);
|
||||
return regex.test(formattedArn) || regex.test(extractPrincipalArn(Arn, true));
|
||||
});
|
||||
|
||||
if (!isArnAllowed) {
|
||||
|
||||
@@ -6,7 +6,7 @@ const twelveDigitRegex = new RE2(/^\d{12}$/);
|
||||
// akhilmhdh: change this to a normal function later. Checked no redosable at the moment
|
||||
|
||||
const arnRegex = new RE2(
|
||||
/^arn:aws:(iam|sts)::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|assumed-role\/[a-zA-Z0-9_.@+*/-]+|\*)$/
|
||||
/^arn:aws(?:-us-gov)?:(iam|sts)::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|assumed-role\/[a-zA-Z0-9_.@+*/-]+|\*)$/
|
||||
);
|
||||
|
||||
export const validateAccountIds = z
|
||||
@@ -55,7 +55,7 @@ export const validatePrincipalArns = z
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*', 'arn:aws:sts::123456789012:assumed-role/RoleName'."
|
||||
"Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*', 'arn:aws:sts::123456789012:assumed-role/RoleName'. GovCloud ARNs (arn:aws-us-gov:...) are also supported."
|
||||
}
|
||||
)
|
||||
// Transform to normalize the spaces around commas
|
||||
|
||||
@@ -11,6 +11,7 @@ import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@@ -408,6 +409,24 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
if (!Object.keys(importedSecrets).length) return {};
|
||||
|
||||
let invalidNameCount = 0;
|
||||
let errorMessage = "";
|
||||
|
||||
for (const [key] of Object.entries(importedSecrets)) {
|
||||
const result = SecretNameSchema.safeParse(key);
|
||||
if (!result.success) {
|
||||
invalidNameCount += 1;
|
||||
if (errorMessage === "") errorMessage = result.error.issues[0]?.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidNameCount > 0) {
|
||||
throw new SecretSyncError({
|
||||
message: `Found ${invalidNameCount} invalid secret name${invalidNameCount === 1 ? "" : "s"}. ${errorMessage}`,
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
const importedSecretMap: TSecretMap = {};
|
||||
|
||||
const secretMap = await $getInfisicalSecrets(secretSync, false);
|
||||
|
||||
Reference in New Issue
Block a user