Compare commits

..

5 Commits

Author SHA1 Message Date
edf6a37fe5 fix lint 2025-03-11 13:08:04 -04:00
f5749e326a remove regex and fix lint 2025-03-11 12:49:55 -04:00
75e0a68b68 remove password regex 2025-03-11 12:46:43 -04:00
6fa41a609b remove char and digit rangs and other requested changes/improvments 2025-03-11 12:28:48 -04:00
16d3bbb67a Add password requirements to dyanmic secret
This will add a new accordion to add custom requirements for the generated password for DB drivers. We can use this pattern for other dynamic secrets too
2025-03-10 23:46:04 -04:00
13 changed files with 551 additions and 231 deletions

View File

@ -1,5 +1,16 @@
import { z } from "zod";
export type PasswordRequirements = {
length: number;
required: {
lowercase: number;
uppercase: number;
digits: number;
symbols: number;
};
allowedSymbols?: string;
};
export enum SqlProviders {
Postgres = "postgres",
MySQL = "mysql2",
@ -100,6 +111,28 @@ export const DynamicSecretSqlDBSchema = z.object({
database: z.string().trim(),
username: z.string().trim(),
password: z.string().trim(),
passwordRequirements: z
.object({
length: z.number().min(1).max(250),
required: z
.object({
lowercase: z.number().min(0),
uppercase: z.number().min(0),
digits: z.number().min(0),
symbols: z.number().min(0)
})
.refine((data) => {
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 250;
}, "Sum of required characters cannot exceed 250"),
allowedSymbols: z.string().optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.optional()
.describe("Password generation requirements"),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),

View File

@ -1,6 +1,6 @@
import { randomInt } from "crypto";
import handlebars from "handlebars";
import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { withGatewayProxy } from "@app/lib/gateway";
@ -8,16 +8,99 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const generatePassword = (provider: SqlProviders) => {
// oracle has limit of 48 password length
const size = provider === SqlProviders.Oracle ? 30 : 48;
const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
return customAlphabet(charset, 48)(size);
const ORACLE_PASSWORD_REQUIREMENTS = {
...DEFAULT_PASSWORD_REQUIREMENTS,
length: 30
};
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
const finalReqs = requirements || defaultReqs;
try {
const { length, required, allowedSymbols } = finalReqs;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};
const generateUsername = (provider: SqlProviders) => {
@ -115,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername(providerInputs.client);
const password = generatePassword(providerInputs.client);
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
try {

View File

@ -1,10 +1,10 @@
import { z } from "zod";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({

View File

@ -7,6 +7,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
@ -25,7 +26,6 @@ import {
TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type";
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
import { logger } from "@app/lib/logger";
type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory;

View File

@ -20,7 +20,6 @@ require (
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
github.com/pion/dtls/v3 v3.0.4
github.com/pion/logging v0.2.3
github.com/pion/turn/v4 v4.0.0
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
@ -91,6 +90,7 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect

View File

@ -484,6 +484,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -590,6 +592,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -640,9 +644,13 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -654,6 +662,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -4,9 +4,7 @@ import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
"time"
@ -18,23 +16,31 @@ import (
)
var gatewayCmd = &cobra.Command{
Example: `infisical gateway`,
Short: "Used to infisical gateway",
Use: "gateway",
Short: "Run the Infisical gateway or manage its systemd service",
Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.",
Example: `infisical gateway --token=<token>
sudo infisical gateway install --token=<token> --domain=<domain>`,
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse token flag")
util.HandleError(err, "Unable to parse flag")
}
if token == nil {
util.HandleError(fmt.Errorf("Token not found"))
}
domain, err := cmd.Flags().GetString("domain")
if err != nil {
util.HandleError(err, "Unable to parse domain flag")
}
// Try to install systemd service if possible
if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
log.Warn().Msgf("Failed to install systemd service: %v", err)
}
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
sigCh := make(chan os.Signal, 1)
@ -104,50 +110,6 @@ var gatewayCmd = &cobra.Command{
},
}
var gatewayInstallCmd = &cobra.Command{
Use: "install",
Short: "Install and enable systemd service for the gateway (requires sudo)",
Long: "Install and enable systemd service for the gateway. Must be run with sudo on Linux.",
Example: "sudo infisical gateway install --token=<token> --domain=<domain>",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
if runtime.GOOS != "linux" {
util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux"))
}
if os.Geteuid() != 0 {
util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges"))
}
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if token == nil {
util.HandleError(fmt.Errorf("Token not found"))
}
domain, err := cmd.Flags().GetString("domain")
if err != nil {
util.HandleError(err, "Unable to parse domain flag")
}
if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
util.HandleError(err, "Failed to install systemd service")
}
enableCmd := exec.Command("systemctl", "enable", "infisical-gateway")
if err := enableCmd.Run(); err != nil {
util.HandleError(err, "Failed to enable systemd service")
}
log.Info().Msg("Successfully installed and enabled infisical-gateway service")
log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway")
},
}
var gatewayRelayCmd = &cobra.Command{
Example: `infisical gateway relay`,
Short: "Used to run infisical gateway relay",
@ -177,12 +139,9 @@ var gatewayRelayCmd = &cobra.Command{
func init() {
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance")
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
gatewayCmd.AddCommand(gatewayInstallCmd)
gatewayCmd.AddCommand(gatewayRelayCmd)
rootCmd.AddCommand(gatewayCmd)
}

View File

@ -17,7 +17,7 @@ After=network.target
[Service]
Type=simple
EnvironmentFile=/etc/infisical/gateway.conf
ExecStart=infisical gateway
ExecStart=/usr/local/bin/infisical gateway
Restart=on-failure
InaccessibleDirectories=/home
PrivateTmp=yes

View File

@ -1,107 +0,0 @@
---
title: "infisical gateway"
description: "Run the Infisical gateway or manage its systemd service"
---
<Tabs>
<Tab title="Run gateway">
```bash
infisical gateway --token=<token>
```
</Tab>
<Tab title="Install service">
```bash
sudo infisical gateway install --token=<token> --domain=<domain>
```
</Tab>
</Tabs>
## Description
Run the Infisical gateway in the foreground or manage its systemd service installation. The gateway allows secure communication between your self-hosted Infisical instance and client applications.
## Subcommands & flags
<Accordion title="infisical gateway" defaultOpen="true">
Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection.
```bash
infisical gateway --token=<token> --domain=<domain>
```
### Flags
<Accordion title="--token">
The machine identity access token to authenticate with Infisical.
```bash
# Example
infisical gateway --token=<token>
```
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the gateway command.
</Accordion>
<Accordion title="--domain">
Domain of your self-hosted Infisical instance.
```bash
# Example
sudo infisical gateway install --domain=https://app.your-domain.com
```
</Accordion>
</Accordion>
<Accordion title="infisical gateway install">
Install and enable the gateway as a systemd service. This command must be run with sudo on Linux.
```bash
sudo infisical gateway install --token=<token> --domain=<domain>
```
### Requirements
- Must be run on Linux
- Must be run with root/sudo privileges
- Requires systemd
### Flags
<Accordion title="--token">
The machine identity access token to authenticate with Infisical.
```bash
# Example
sudo infisical gateway install --token=<token>
```
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command.
</Accordion>
<Accordion title="--domain">
Domain of your self-hosted Infisical instance.
```bash
# Example
sudo infisical gateway install --domain=https://app.your-domain.com
```
</Accordion>
### Service Details
The systemd service is installed with secure defaults:
- Service file: `/etc/systemd/system/infisical-gateway.service`
- Config file: `/etc/infisical/gateway.conf`
- Runs with restricted privileges:
- InaccessibleDirectories=/home
- PrivateTmp=yes
- Resource limits configured for stability
- Automatically restarts on failure
- Enabled to start on boot
After installation, manage the service with standard systemd commands:
```bash
sudo systemctl start infisical-gateway # Start the service
sudo systemctl stop infisical-gateway # Stop the service
sudo systemctl status infisical-gateway # Check service status
sudo systemctl disable infisical-gateway # Disable auto-start on boot
```
</Accordion>

View File

@ -45,53 +45,19 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
</Step>
<Step title="Deploy the Gateway">
Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production:
<Tabs>
<Tab title="Production (systemd)">
For production deployments on Linux, install the Gateway as a systemd service:
```bash
sudo infisical gateway install --token <your-machine-identity-token> --domain <your-infisical-domain>
sudo systemctl start infisical-gateway
```
This will install and start the Gateway as a secure systemd service that:
- Runs with restricted privileges:
- Runs as root user (required for secure token management)
- Restricted access to home directories
- Private temporary directory
- Automatically restarts on failure
- Starts on system boot
- Manages token and domain configuration securely in `/etc/infisical/gateway.conf`
<Warning>
The install command requires:
- Linux operating system
- Root/sudo privileges
- Systemd
</Warning>
</Tab>
<Tab title="Development (direct)">
For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command:
```bash
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
```
Alternatively, if you already have the token, use it directly with the `--token` flag:
```bash
infisical gateway --token <your-machine-identity-token>
```
Or set it as an environment variable:
```bash
export INFISICAL_TOKEN=<your-machine-identity-token>
infisical gateway
```
</Tab>
</Tabs>
For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway).
Use the Infisical CLI to deploy the Gateway. You can log in with your machine identity and start the Gateway in one command. The example below demonstrates how to deploy the Gateway using the Universal Auth method:
```bash
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
```
Alternatively, if you already have the token, use it directly with the `--token` flag:
```bash
infisical gateway --token <your-machine-identity-token>
```
Or set it as an environment variable:
```bash
export INFISICAL_TOKEN=<your-machine-identity-token>
infisical gateway
```
<Note>
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
</Note>
@ -112,3 +78,4 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
</Step>
</Steps>

View File

@ -339,7 +339,6 @@
"cli/commands/secrets",
"cli/commands/dynamic-secrets",
"cli/commands/ssh",
"cli/commands/gateway",
"cli/commands/export",
"cli/commands/token",
"cli/commands/service-token",

View File

@ -23,6 +23,23 @@ import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
const passwordRequirementsSchema = z.object({
length: z.number().min(1).max(250),
required: z.object({
lowercase: z.number().min(0),
uppercase: z.number().min(0),
digits: z.number().min(0),
symbols: z.number().min(0)
}).refine((data) => {
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 250;
}, "Sum of required characters cannot exceed 250"),
allowedSymbols: z.string().optional()
}).refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length");
const formSchema = z.object({
provider: z.object({
client: z.nativeEnum(SqlProviders),
@ -31,6 +48,7 @@ const formSchema = z.object({
database: z.string().min(1),
username: z.string().min(1),
password: z.string().min(1),
passwordRequirements: passwordRequirementsSchema.optional(),
creationStatement: z.string().min(1),
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
@ -133,11 +151,24 @@ export const SqlDatabaseInputForm = ({
control,
setValue,
formState: { isSubmitting },
handleSubmit
handleSubmit,
watch
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues: {
provider: getSqlStatements(SqlProviders.Postgres)
provider: {
...getSqlStatements(SqlProviders.Postgres),
passwordRequirements: {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: '-_.~!*'
}
}
}
});
@ -174,6 +205,10 @@ export const SqlDatabaseInputForm = ({
setValue("provider.renewStatement", sqlStatment.renewStatement);
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
setValue("provider.port", getDefaultPort(type));
// Update password requirements based on provider
const length = type === SqlProviders.Oracle ? 30 : 48;
setValue("provider.passwordRequirements.length", length);
};
return (
@ -197,6 +232,7 @@ export const SqlDatabaseInputForm = ({
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
@ -386,10 +422,13 @@ export const SqlDatabaseInputForm = ({
</FormControl>
)}
/>
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
<AccordionItem value="advance-statements">
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
<Accordion type="multiple" className="mb-2 w-full bg-mineshaft-700">
<AccordionItem value="advanced">
<AccordionTrigger>Creation, Revocation & Renew Statements (optional)</AccordionTrigger>
<AccordionContent>
<div className="mb-4 text-sm text-mineshaft-300">
Customize SQL statements for managing database user lifecycle
</div>
<Controller
control={control}
name="provider.creationStatement"
@ -450,6 +489,157 @@ export const SqlDatabaseInputForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
<AccordionItem value="password-config">
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
<AccordionContent>
<div className="mb-4 text-sm text-mineshaft-300">
Set constraints on the generated database password
</div>
<div className="space-y-4">
<div>
<Controller
control={control}
name="provider.passwordRequirements.length"
defaultValue={48}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password Length"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
type="number"
min={1}
max={250}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
<div className="text-sm text-gray-500">
{(() => {
const total = Object.values(watch("provider.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0);
const length = watch("provider.passwordRequirements.length") || 0;
const isError = total > length;
return (
<span className={isError ? "text-red-500" : ""}>
Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""}
</span>
);
})()}
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="provider.passwordRequirements.required.lowercase"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Lowercase Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of lowercase letters"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.passwordRequirements.required.uppercase"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Uppercase Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of uppercase letters"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.passwordRequirements.required.digits"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digit Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of digits"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.passwordRequirements.required.symbols"
defaultValue={0}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Symbol Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of symbols"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium">Allowed Symbols</h4>
<Controller
control={control}
name="provider.passwordRequirements.allowedSymbols"
defaultValue="-_.~!*"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Symbols to use in password"
isError={Boolean(error)}
errorText={error?.message}
helperText="Default: -_.~!*"
>
<Input {...field} placeholder="-_.~!*" />
</FormControl>
)}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>

View File

@ -23,6 +23,23 @@ import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
const passwordRequirementsSchema = z.object({
length: z.number().min(1).max(250),
required: z.object({
lowercase: z.number().min(0),
uppercase: z.number().min(0),
digits: z.number().min(0),
symbols: z.number().min(0)
}).refine((data) => {
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 250; // Sanity check for individual validation
}, "Sum of required characters cannot exceed 250"),
allowedSymbols: z.string().optional()
}).refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length");
const formSchema = z.object({
inputs: z
.object({
@ -32,6 +49,7 @@ const formSchema = z.object({
database: z.string().min(1),
username: z.string().min(1),
password: z.string().min(1),
passwordRequirements: passwordRequirementsSchema.optional(),
creationStatement: z.string().min(1),
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
@ -82,6 +100,17 @@ export const EditDynamicSecretSqlProviderForm = ({
secretPath,
projectSlug
}: Props) => {
const getDefaultPasswordRequirements = (provider: SqlProviders) => ({
length: provider === SqlProviders.Oracle ? 30 : 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: '-_.~!*'
});
const {
control,
watch,
@ -94,10 +123,13 @@ export const EditDynamicSecretSqlProviderForm = ({
maxTTL: dynamicSecret.maxTTL,
newName: dynamicSecret.name,
inputs: {
...(dynamicSecret.inputs as TForm["inputs"])
...(dynamicSecret.inputs as TForm["inputs"]),
passwordRequirements: (dynamicSecret.inputs as TForm["inputs"])?.passwordRequirements ||
getDefaultPasswordRequirements((dynamicSecret.inputs as TForm["inputs"])?.client || SqlProviders.Postgres)
}
}
});
const { currentWorkspace } = useWorkspace();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
@ -347,10 +379,13 @@ export const EditDynamicSecretSqlProviderForm = ({
</FormControl>
)}
/>
<Accordion type="multiple" className="w-full bg-mineshaft-700">
<AccordionItem value="modify-sql-statement">
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
<AccordionItem value="advanced">
<AccordionTrigger>Creation, Revocation & Renew Statements (optional)</AccordionTrigger>
<AccordionContent>
<div className="mb-4 text-sm text-mineshaft-300">
Customize SQL statements for managing database user lifecycle
</div>
<Controller
control={control}
name="inputs.creationStatement"
@ -418,6 +453,157 @@ export const EditDynamicSecretSqlProviderForm = ({
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
<AccordionItem value="password-config">
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
<AccordionContent>
<div className="mb-4 text-sm text-mineshaft-300">
Set constraints on the generated database password
</div>
<div className="space-y-4">
<div>
<Controller
control={control}
name="inputs.passwordRequirements.length"
defaultValue={48}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password Length"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
type="number"
min={1}
max={250}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
<div className="text-sm text-gray-500">
{(() => {
const total = Object.values(watch("inputs.passwordRequirements.required") || {}).reduce((sum, count) => sum + Number(count || 0), 0);
const length = watch("inputs.passwordRequirements.length") || 0;
const isError = total > length;
return (
<span className={isError ? "text-red-500" : ""}>
Total required characters: {total} {isError ? `(exceeds length of ${length})` : ""}
</span>
);
})()}
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="inputs.passwordRequirements.required.lowercase"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Lowercase Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of lowercase letters"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.passwordRequirements.required.uppercase"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Uppercase Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of uppercase letters"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.passwordRequirements.required.digits"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digit Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of digits"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.passwordRequirements.required.symbols"
defaultValue={0}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Symbol Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of symbols"
>
<Input
type="number"
min={0}
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
</div>
</div>
<div className="space-y-2">
<h4 className="text-sm font-medium">Allowed Symbols</h4>
<Controller
control={control}
name="inputs.passwordRequirements.allowedSymbols"
defaultValue="-_.~!*"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Symbols to use in password"
isError={Boolean(error)}
errorText={error?.message}
helperText="Default: -_.~!*"
>
<Input {...field} placeholder="-_.~!*" />
</FormControl>
)}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</div>