diff --git a/.github/workflows/run-cli-e2e-tests.yml b/.github/workflows/run-cli-e2e-tests.yml index 8d2f20e..db546be 100644 --- a/.github/workflows/run-cli-e2e-tests.yml +++ b/.github/workflows/run-cli-e2e-tests.yml @@ -9,14 +9,14 @@ on: jobs: test: runs-on: ubuntu-latest - name: General E2E Tests + name: CLI End-to-End Testing steps: - uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: - go-version: "1.24.13" + go-version: "1.25.2" - name: Install dependencies run: go get . - name: Build the CLI @@ -26,7 +26,6 @@ jobs: with: repository: infisical/infisical path: infisical - - name: Free disk space run: | sudo rm -rf /usr/share/dotnet @@ -45,7 +44,7 @@ jobs: agent-test: runs-on: ubuntu-latest - name: Agent E2E Tests + name: Agent End-to-End Testing steps: - uses: actions/checkout@v6 @@ -69,3 +68,38 @@ jobs: INFISICAL_BACKEND_DIR: ${{ github.workspace }}/infisical/backend INFISICAL_CLI_EXECUTABLE: ${{ github.workspace }}/infisical-cli CLI_E2E_DEFAULT_RUN_METHOD: subprocess + pam-test: + runs-on: ubuntu-latest + name: PAM End-to-End Testing + + steps: + - uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: "1.25.2" + - name: Install dependencies + run: go get . + - name: Build the CLI + run: go build -o infisical-cli + - name: Checkout infisical repo + uses: actions/checkout@v6 + with: + repository: infisical/infisical + path: infisical + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + docker system prune -af + df -h + - name: Test PAM Resources + run: go test -v -timeout 30m -count=1 github.com/infisical/cli/e2e-tests/pam + working-directory: ./e2e + env: + INFISICAL_BACKEND_DIR: ${{ github.workspace }}/infisical/backend + INFISICAL_CLI_EXECUTABLE: ${{ github.workspace }}/infisical-cli + CLI_E2E_DEFAULT_RUN_METHOD: subprocess + diff --git a/e2e/README.md b/e2e/README.md index b5e9fba..7b84acc 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -171,8 +171,11 @@ If you're using a `.env` file (recommended), just make sure it's configured and cd e2e go test github.com/infisical/cli/e2e-tests/relay go test github.com/infisical/cli/e2e-tests/agent +go test github.com/infisical/cli/e2e-tests/pam ``` +**Note:** PAM tests use subprocess mode and require a built CLI binary. Build it first with `go build -o e2e/infisical-merge .` from the repo root. + Alternatively, you can export environment variables manually: ```bash @@ -181,6 +184,7 @@ export INFISICAL_BACKEND_DIR=/path/to/infisical/backend cd e2e go test github.com/infisical/cli/e2e-tests/relay go test github.com/infisical/cli/e2e-tests/agent +go test github.com/infisical/cli/e2e-tests/pam ``` **Tip:** Using a `.env` file is much more convenient than exporting variables manually. See the [Environment Variables Configuration](#environment-variables-configuration) section above for details. diff --git a/e2e/go.mod b/e2e/go.mod index 0818c25..0d4f479 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -20,6 +20,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/compose v0.40.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 github.com/testcontainers/testcontainers-go/modules/redis v0.40.0 ) diff --git a/e2e/go.sum b/e2e/go.sum index 6f6ecc8..cc33a8d 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -596,6 +596,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -930,6 +932,8 @@ github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+ github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= github.com/testcontainers/testcontainers-go/modules/compose v0.40.0 h1:Bj8W7GieY56sRbVJx1yLh0JVEtOQ8SQMhX+jRtzenLA= github.com/testcontainers/testcontainers-go/modules/compose v0.40.0/go.mod h1:fEEGqtsoH1KS+sUi1WG4+vH3fqdCyip1U9Hd8P3SRMA= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ= github.com/testcontainers/testcontainers-go/modules/redis v0.40.0 h1:OG4qwcxp2O0re7V7M9lY9w0v6wWgWf7j7rtkpAnGMd0= github.com/testcontainers/testcontainers-go/modules/redis v0.40.0/go.mod h1:Bc+EDhKMo5zI5V5zdBkHiMVzeAXbtI4n5isS/nzf6zw= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= diff --git a/e2e/openapi-cfg.yaml b/e2e/openapi-cfg.yaml index 8cc7071..2cbd9bb 100644 --- a/e2e/openapi-cfg.yaml +++ b/e2e/openapi-cfg.yaml @@ -32,3 +32,5 @@ output-options: - attachUniversalAuth - createUniversalAuthClientSecret - createCloudflareAppConnection + - createPostgresPamResource + - createPostgresPamAccount diff --git a/e2e/packages/client/client.gen.go b/e2e/packages/client/client.gen.go index f2e23aa..c63c2a9 100644 --- a/e2e/packages/client/client.gen.go +++ b/e2e/packages/client/client.gen.go @@ -745,6 +745,25 @@ type CreateMachineIdentityJSONBody struct { Role *string `json:"role,omitempty"` } +// CreatePostgresPamAccountJSONBody defines parameters for CreatePostgresPamAccount. +type CreatePostgresPamAccountJSONBody struct { + Credentials struct { + Password string `json:"password"` + Username string `json:"username"` + } `json:"credentials"` + Description *string `json:"description"` + FolderId *openapi_types.UUID `json:"folderId,omitempty"` + Metadata *[]struct { + Key string `json:"key"` + Value *string `json:"value,omitempty"` + } `json:"metadata,omitempty"` + Name string `json:"name"` + RequireMfa *bool `json:"requireMfa,omitempty"` + ResourceId openapi_types.UUID `json:"resourceId"` + RotationEnabled bool `json:"rotationEnabled"` + RotationIntervalSeconds *float32 `json:"rotationIntervalSeconds"` +} + // CreateKubernetesPamResourceJSONBody defines parameters for CreateKubernetesPamResource. type CreateKubernetesPamResourceJSONBody struct { ConnectionDetails struct { @@ -776,6 +795,29 @@ type CreateKubernetesPamResourceJSONBody_RotationAccountCredentials struct { union json.RawMessage } +// CreatePostgresPamResourceJSONBody defines parameters for CreatePostgresPamResource. +type CreatePostgresPamResourceJSONBody struct { + ConnectionDetails struct { + Database string `json:"database"` + Host string `json:"host"` + Port float32 `json:"port"` + SslCertificate *string `json:"sslCertificate,omitempty"` + SslEnabled bool `json:"sslEnabled"` + SslRejectUnauthorized bool `json:"sslRejectUnauthorized"` + } `json:"connectionDetails"` + GatewayId openapi_types.UUID `json:"gatewayId"` + Metadata *[]struct { + Key string `json:"key"` + Value *string `json:"value,omitempty"` + } `json:"metadata,omitempty"` + Name string `json:"name"` + ProjectId openapi_types.UUID `json:"projectId"` + RotationAccountCredentials *struct { + Password string `json:"password"` + Username string `json:"username"` + } `json:"rotationAccountCredentials"` +} + // CreateRedisPamResourceJSONBody defines parameters for CreateRedisPamResource. type CreateRedisPamResourceJSONBody struct { ConnectionDetails struct { @@ -1060,9 +1102,15 @@ type CreateCertificateProfileJSONRequestBody CreateCertificateProfileJSONBody // CreateMachineIdentityJSONRequestBody defines body for CreateMachineIdentity for application/json ContentType. type CreateMachineIdentityJSONRequestBody CreateMachineIdentityJSONBody +// CreatePostgresPamAccountJSONRequestBody defines body for CreatePostgresPamAccount for application/json ContentType. +type CreatePostgresPamAccountJSONRequestBody CreatePostgresPamAccountJSONBody + // CreateKubernetesPamResourceJSONRequestBody defines body for CreateKubernetesPamResource for application/json ContentType. type CreateKubernetesPamResourceJSONRequestBody CreateKubernetesPamResourceJSONBody +// CreatePostgresPamResourceJSONRequestBody defines body for CreatePostgresPamResource for application/json ContentType. +type CreatePostgresPamResourceJSONRequestBody CreatePostgresPamResourceJSONBody + // CreateRedisPamResourceJSONRequestBody defines body for CreateRedisPamResource for application/json ContentType. type CreateRedisPamResourceJSONRequestBody CreateRedisPamResourceJSONBody @@ -1217,11 +1265,21 @@ type ClientInterface interface { CreateMachineIdentity(ctx context.Context, body CreateMachineIdentityJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreatePostgresPamAccountWithBody request with any body + CreatePostgresPamAccountWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreatePostgresPamAccount(ctx context.Context, body CreatePostgresPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateKubernetesPamResourceWithBody request with any body CreateKubernetesPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) CreateKubernetesPamResource(ctx context.Context, body CreateKubernetesPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreatePostgresPamResourceWithBody request with any body + CreatePostgresPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreatePostgresPamResource(ctx context.Context, body CreatePostgresPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateRedisPamResourceWithBody request with any body CreateRedisPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1565,6 +1623,30 @@ func (c *Client) CreateMachineIdentity(ctx context.Context, body CreateMachineId return c.Client.Do(req) } +func (c *Client) CreatePostgresPamAccountWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePostgresPamAccountRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreatePostgresPamAccount(ctx context.Context, body CreatePostgresPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePostgresPamAccountRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) CreateKubernetesPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewCreateKubernetesPamResourceRequestWithBody(c.Server, contentType, body) if err != nil { @@ -1589,6 +1671,30 @@ func (c *Client) CreateKubernetesPamResource(ctx context.Context, body CreateKub return c.Client.Do(req) } +func (c *Client) CreatePostgresPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePostgresPamResourceRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreatePostgresPamResource(ctx context.Context, body CreatePostgresPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePostgresPamResourceRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) CreateRedisPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewCreateRedisPamResourceRequestWithBody(c.Server, contentType, body) if err != nil { @@ -2323,6 +2429,46 @@ func NewCreateMachineIdentityRequestWithBody(server string, contentType string, return req, nil } +// NewCreatePostgresPamAccountRequest calls the generic CreatePostgresPamAccount builder with application/json body +func NewCreatePostgresPamAccountRequest(server string, body CreatePostgresPamAccountJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreatePostgresPamAccountRequestWithBody(server, "application/json", bodyReader) +} + +// NewCreatePostgresPamAccountRequestWithBody generates requests for CreatePostgresPamAccount with any type of body +func NewCreatePostgresPamAccountRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/pam/accounts/postgres") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewCreateKubernetesPamResourceRequest calls the generic CreateKubernetesPamResource builder with application/json body func NewCreateKubernetesPamResourceRequest(server string, body CreateKubernetesPamResourceJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -2363,6 +2509,46 @@ func NewCreateKubernetesPamResourceRequestWithBody(server string, contentType st return req, nil } +// NewCreatePostgresPamResourceRequest calls the generic CreatePostgresPamResource builder with application/json body +func NewCreatePostgresPamResourceRequest(server string, body CreatePostgresPamResourceJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreatePostgresPamResourceRequestWithBody(server, "application/json", bodyReader) +} + +// NewCreatePostgresPamResourceRequestWithBody generates requests for CreatePostgresPamResource with any type of body +func NewCreatePostgresPamResourceRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/pam/resources/postgres") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewCreateRedisPamResourceRequest calls the generic CreateRedisPamResource builder with application/json body func NewCreateRedisPamResourceRequest(server string, body CreateRedisPamResourceJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -3141,11 +3327,21 @@ type ClientWithResponsesInterface interface { CreateMachineIdentityWithResponse(ctx context.Context, body CreateMachineIdentityJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateMachineIdentityResponse, error) + // CreatePostgresPamAccountWithBodyWithResponse request with any body + CreatePostgresPamAccountWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePostgresPamAccountResponse, error) + + CreatePostgresPamAccountWithResponse(ctx context.Context, body CreatePostgresPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePostgresPamAccountResponse, error) + // CreateKubernetesPamResourceWithBodyWithResponse request with any body CreateKubernetesPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateKubernetesPamResourceResponse, error) CreateKubernetesPamResourceWithResponse(ctx context.Context, body CreateKubernetesPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateKubernetesPamResourceResponse, error) + // CreatePostgresPamResourceWithBodyWithResponse request with any body + CreatePostgresPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePostgresPamResourceResponse, error) + + CreatePostgresPamResourceWithResponse(ctx context.Context, body CreatePostgresPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePostgresPamResourceResponse, error) + // CreateRedisPamResourceWithBodyWithResponse request with any body CreateRedisPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateRedisPamResourceResponse, error) @@ -4446,6 +4642,108 @@ func (r CreateMachineIdentityResponse) StatusCode() int { return 0 } +type CreatePostgresPamAccountResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Account struct { + CreatedAt time.Time `json:"createdAt"` + Credentials struct { + Username string `json:"username"` + } `json:"credentials"` + Description *string `json:"description"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` + EncryptedLastRotationMessage interface{} `json:"encryptedLastRotationMessage"` + FolderId *openapi_types.UUID `json:"folderId"` + Id openapi_types.UUID `json:"id"` + InternalMetadata interface{} `json:"internalMetadata"` + LastRotatedAt *time.Time `json:"lastRotatedAt"` + LastRotationMessage *string `json:"lastRotationMessage"` + Metadata *[]struct { + Id openapi_types.UUID `json:"id"` + Key string `json:"key"` + Value *string `json:"value"` + } `json:"metadata,omitempty"` + Name string `json:"name"` + ProjectId string `json:"projectId"` + RequireMfa *bool `json:"requireMfa"` + Resource struct { + Id openapi_types.UUID `json:"id"` + Name string `json:"name"` + ResourceType string `json:"resourceType"` + RotationCredentialsConfigured bool `json:"rotationCredentialsConfigured"` + } `json:"resource"` + ResourceId openapi_types.UUID `json:"resourceId"` + ResourceType CreatePostgresPamAccount200AccountResourceType `json:"resourceType"` + RotationEnabled *bool `json:"rotationEnabled,omitempty"` + RotationIntervalSeconds *float32 `json:"rotationIntervalSeconds"` + RotationStatus *string `json:"rotationStatus"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"account"` + } + JSON400 *struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount400StatusCode `json:"statusCode"` + } + JSON401 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount401StatusCode `json:"statusCode"` + } + JSON403 *struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount403StatusCode `json:"statusCode"` + } + JSON404 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount404StatusCode `json:"statusCode"` + } + JSON422 *struct { + Error string `json:"error"` + Message interface{} `json:"message,omitempty"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount422StatusCode `json:"statusCode"` + } + JSON500 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount500StatusCode `json:"statusCode"` + } +} +type CreatePostgresPamAccount200AccountResourceType string +type CreatePostgresPamAccount400StatusCode float32 +type CreatePostgresPamAccount401StatusCode float32 +type CreatePostgresPamAccount403StatusCode float32 +type CreatePostgresPamAccount404StatusCode float32 +type CreatePostgresPamAccount422StatusCode float32 +type CreatePostgresPamAccount500StatusCode float32 + +// Status returns HTTPResponse.Status +func (r CreatePostgresPamAccountResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreatePostgresPamAccountResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type CreateKubernetesPamResourceResponse struct { Body []byte HTTPResponse *http.Response @@ -4458,6 +4756,7 @@ type CreateKubernetesPamResourceResponse struct { Url string `json:"url"` } `json:"connectionDetails"` CreatedAt time.Time `json:"createdAt"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` EncryptedResourceMetadata interface{} `json:"encryptedResourceMetadata"` GatewayId *openapi_types.UUID `json:"gatewayId"` Id openapi_types.UUID `json:"id"` @@ -4543,6 +4842,102 @@ func (r CreateKubernetesPamResourceResponse) StatusCode() int { return 0 } +type CreatePostgresPamResourceResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Resource struct { + AdServerResourceId *openapi_types.UUID `json:"adServerResourceId"` + ConnectionDetails struct { + Database string `json:"database"` + Host string `json:"host"` + Port float32 `json:"port"` + SslCertificate *string `json:"sslCertificate,omitempty"` + SslEnabled bool `json:"sslEnabled"` + SslRejectUnauthorized bool `json:"sslRejectUnauthorized"` + } `json:"connectionDetails"` + CreatedAt time.Time `json:"createdAt"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` + EncryptedResourceMetadata interface{} `json:"encryptedResourceMetadata"` + GatewayId *openapi_types.UUID `json:"gatewayId"` + Id openapi_types.UUID `json:"id"` + Metadata *[]struct { + Id openapi_types.UUID `json:"id"` + Key string `json:"key"` + Value *string `json:"value"` + } `json:"metadata,omitempty"` + Name string `json:"name"` + ProjectId string `json:"projectId"` + ResourceType CreatePostgresPamResource200ResourceResourceType `json:"resourceType"` + RotationAccountCredentials *struct { + Username string `json:"username"` + } `json:"rotationAccountCredentials"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"resource"` + } + JSON400 *struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource400StatusCode `json:"statusCode"` + } + JSON401 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource401StatusCode `json:"statusCode"` + } + JSON403 *struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource403StatusCode `json:"statusCode"` + } + JSON404 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource404StatusCode `json:"statusCode"` + } + JSON422 *struct { + Error string `json:"error"` + Message interface{} `json:"message,omitempty"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource422StatusCode `json:"statusCode"` + } + JSON500 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource500StatusCode `json:"statusCode"` + } +} +type CreatePostgresPamResource200ResourceResourceType string +type CreatePostgresPamResource400StatusCode float32 +type CreatePostgresPamResource401StatusCode float32 +type CreatePostgresPamResource403StatusCode float32 +type CreatePostgresPamResource404StatusCode float32 +type CreatePostgresPamResource422StatusCode float32 +type CreatePostgresPamResource500StatusCode float32 + +// Status returns HTTPResponse.Status +func (r CreatePostgresPamResourceResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreatePostgresPamResourceResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type CreateRedisPamResourceResponse struct { Body []byte HTTPResponse *http.Response @@ -4557,6 +4952,7 @@ type CreateRedisPamResourceResponse struct { SslRejectUnauthorized bool `json:"sslRejectUnauthorized"` } `json:"connectionDetails"` CreatedAt time.Time `json:"createdAt"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` EncryptedResourceMetadata interface{} `json:"encryptedResourceMetadata"` GatewayId *openapi_types.UUID `json:"gatewayId"` Id openapi_types.UUID `json:"id"` @@ -5750,6 +6146,23 @@ func (c *ClientWithResponses) CreateMachineIdentityWithResponse(ctx context.Cont return ParseCreateMachineIdentityResponse(rsp) } +// CreatePostgresPamAccountWithBodyWithResponse request with arbitrary body returning *CreatePostgresPamAccountResponse +func (c *ClientWithResponses) CreatePostgresPamAccountWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePostgresPamAccountResponse, error) { + rsp, err := c.CreatePostgresPamAccountWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePostgresPamAccountResponse(rsp) +} + +func (c *ClientWithResponses) CreatePostgresPamAccountWithResponse(ctx context.Context, body CreatePostgresPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePostgresPamAccountResponse, error) { + rsp, err := c.CreatePostgresPamAccount(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePostgresPamAccountResponse(rsp) +} + // CreateKubernetesPamResourceWithBodyWithResponse request with arbitrary body returning *CreateKubernetesPamResourceResponse func (c *ClientWithResponses) CreateKubernetesPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateKubernetesPamResourceResponse, error) { rsp, err := c.CreateKubernetesPamResourceWithBody(ctx, contentType, body, reqEditors...) @@ -5767,6 +6180,23 @@ func (c *ClientWithResponses) CreateKubernetesPamResourceWithResponse(ctx contex return ParseCreateKubernetesPamResourceResponse(rsp) } +// CreatePostgresPamResourceWithBodyWithResponse request with arbitrary body returning *CreatePostgresPamResourceResponse +func (c *ClientWithResponses) CreatePostgresPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePostgresPamResourceResponse, error) { + rsp, err := c.CreatePostgresPamResourceWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePostgresPamResourceResponse(rsp) +} + +func (c *ClientWithResponses) CreatePostgresPamResourceWithResponse(ctx context.Context, body CreatePostgresPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePostgresPamResourceResponse, error) { + rsp, err := c.CreatePostgresPamResource(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePostgresPamResourceResponse(rsp) +} + // CreateRedisPamResourceWithBodyWithResponse request with arbitrary body returning *CreateRedisPamResourceResponse func (c *ClientWithResponses) CreateRedisPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateRedisPamResourceResponse, error) { rsp, err := c.CreateRedisPamResourceWithBody(ctx, contentType, body, reqEditors...) @@ -7541,6 +7971,141 @@ func ParseCreateMachineIdentityResponse(rsp *http.Response) (*CreateMachineIdent return response, nil } +// ParseCreatePostgresPamAccountResponse parses an HTTP response from a CreatePostgresPamAccountWithResponse call +func ParseCreatePostgresPamAccountResponse(rsp *http.Response) (*CreatePostgresPamAccountResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreatePostgresPamAccountResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Account struct { + CreatedAt time.Time `json:"createdAt"` + Credentials struct { + Username string `json:"username"` + } `json:"credentials"` + Description *string `json:"description"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` + EncryptedLastRotationMessage interface{} `json:"encryptedLastRotationMessage"` + FolderId *openapi_types.UUID `json:"folderId"` + Id openapi_types.UUID `json:"id"` + InternalMetadata interface{} `json:"internalMetadata"` + LastRotatedAt *time.Time `json:"lastRotatedAt"` + LastRotationMessage *string `json:"lastRotationMessage"` + Metadata *[]struct { + Id openapi_types.UUID `json:"id"` + Key string `json:"key"` + Value *string `json:"value"` + } `json:"metadata,omitempty"` + Name string `json:"name"` + ProjectId string `json:"projectId"` + RequireMfa *bool `json:"requireMfa"` + Resource struct { + Id openapi_types.UUID `json:"id"` + Name string `json:"name"` + ResourceType string `json:"resourceType"` + RotationCredentialsConfigured bool `json:"rotationCredentialsConfigured"` + } `json:"resource"` + ResourceId openapi_types.UUID `json:"resourceId"` + ResourceType CreatePostgresPamAccount200AccountResourceType `json:"resourceType"` + RotationEnabled *bool `json:"rotationEnabled,omitempty"` + RotationIntervalSeconds *float32 `json:"rotationIntervalSeconds"` + RotationStatus *string `json:"rotationStatus"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"account"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount400StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount401StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount403StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount404StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest struct { + Error string `json:"error"` + Message interface{} `json:"message,omitempty"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount422StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamAccount500StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseCreateKubernetesPamResourceResponse parses an HTTP response from a CreateKubernetesPamResourceWithResponse call func ParseCreateKubernetesPamResourceResponse(rsp *http.Response) (*CreateKubernetesPamResourceResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -7565,6 +8130,7 @@ func ParseCreateKubernetesPamResourceResponse(rsp *http.Response) (*CreateKubern Url string `json:"url"` } `json:"connectionDetails"` CreatedAt time.Time `json:"createdAt"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` EncryptedResourceMetadata interface{} `json:"encryptedResourceMetadata"` GatewayId *openapi_types.UUID `json:"gatewayId"` Id openapi_types.UUID `json:"id"` @@ -7664,6 +8230,135 @@ func ParseCreateKubernetesPamResourceResponse(rsp *http.Response) (*CreateKubern return response, nil } +// ParseCreatePostgresPamResourceResponse parses an HTTP response from a CreatePostgresPamResourceWithResponse call +func ParseCreatePostgresPamResourceResponse(rsp *http.Response) (*CreatePostgresPamResourceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreatePostgresPamResourceResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + Resource struct { + AdServerResourceId *openapi_types.UUID `json:"adServerResourceId"` + ConnectionDetails struct { + Database string `json:"database"` + Host string `json:"host"` + Port float32 `json:"port"` + SslCertificate *string `json:"sslCertificate,omitempty"` + SslEnabled bool `json:"sslEnabled"` + SslRejectUnauthorized bool `json:"sslRejectUnauthorized"` + } `json:"connectionDetails"` + CreatedAt time.Time `json:"createdAt"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` + EncryptedResourceMetadata interface{} `json:"encryptedResourceMetadata"` + GatewayId *openapi_types.UUID `json:"gatewayId"` + Id openapi_types.UUID `json:"id"` + Metadata *[]struct { + Id openapi_types.UUID `json:"id"` + Key string `json:"key"` + Value *string `json:"value"` + } `json:"metadata,omitempty"` + Name string `json:"name"` + ProjectId string `json:"projectId"` + ResourceType CreatePostgresPamResource200ResourceResourceType `json:"resourceType"` + RotationAccountCredentials *struct { + Username string `json:"username"` + } `json:"rotationAccountCredentials"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"resource"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource400StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource401StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource403StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource404StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest struct { + Error string `json:"error"` + Message interface{} `json:"message,omitempty"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource422StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreatePostgresPamResource500StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseCreateRedisPamResourceResponse parses an HTTP response from a CreateRedisPamResourceWithResponse call func ParseCreateRedisPamResourceResponse(rsp *http.Response) (*CreateRedisPamResourceResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -7690,6 +8385,7 @@ func ParseCreateRedisPamResourceResponse(rsp *http.Response) (*CreateRedisPamRes SslRejectUnauthorized bool `json:"sslRejectUnauthorized"` } `json:"connectionDetails"` CreatedAt time.Time `json:"createdAt"` + DiscoveryFingerprint *string `json:"discoveryFingerprint"` EncryptedResourceMetadata interface{} `json:"encryptedResourceMetadata"` GatewayId *openapi_types.UUID `json:"gatewayId"` Id openapi_types.UUID `json:"id"` diff --git a/e2e/packages/client/provisioner.go b/e2e/packages/client/provisioner.go index a18fb01..1fe85f4 100644 --- a/e2e/packages/client/provisioner.go +++ b/e2e/packages/client/provisioner.go @@ -16,9 +16,11 @@ type Provisioner struct { } type ProvisionResult struct { - UserId string - OrgId string - Token string + UserId string + OrgId string + Token string + Email string + Password string } type ProvisionerOption func(*Provisioner) @@ -51,10 +53,12 @@ func WithCookies(cookies ...*http.Cookie) RequestEditorFn { func (p *Provisioner) Bootstrap(ctx context.Context) (*ProvisionResult, error) { slog.Info("Signing up Admin account ...") + email := faker.Email() + password := faker.Password() signUpResp, err := p.Client.AdminSignUpWithResponse(ctx, AdminSignUpJSONRequestBody{ - Email: types.Email(faker.Email()), + Email: types.Email(email), FirstName: faker.FirstName(), - Password: faker.Password(), + Password: password, }) if err != nil { return nil, err @@ -105,8 +109,10 @@ func (p *Provisioner) Bootstrap(ctx context.Context) (*ProvisionResult, error) { } slog.Info("Token successfully created") return &ProvisionResult{ - UserId: signUpResp.JSON200.User.Id.String(), - OrgId: signUpResp.JSON200.Organization.Id.String(), - Token: authTokenResp.JSON200.Token, + UserId: signUpResp.JSON200.User.Id.String(), + OrgId: signUpResp.JSON200.Organization.Id.String(), + Token: authTokenResp.JSON200.Token, + Email: email, + Password: password, }, nil } diff --git a/e2e/pam/pam_helpers.go b/e2e/pam/pam_helpers.go new file mode 100644 index 0000000..c00d97d --- /dev/null +++ b/e2e/pam/pam_helpers.go @@ -0,0 +1,201 @@ +package pam_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/compose-spec/compose-go/v2/types" + "github.com/infisical/cli/e2e-tests/packages/client" + helpers "github.com/infisical/cli/e2e-tests/util" + openapitypes "github.com/oapi-codegen/runtime/types" + "github.com/stretchr/testify/require" +) + +// getOutboundIP returns a non-loopback IPv4 address of the host. +// This IP is reachable from both Docker containers and host processes, +// unlike "host.docker.internal" which only resolves inside Docker. +// It enumerates local interfaces instead of dialing an external address, +// so it works in air-gapped and network-restricted CI environments. +func getOutboundIP(t *testing.T) string { + addrs, err := net.InterfaceAddrs() + require.NoError(t, err) + for _, addr := range addrs { + if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil { + return ipNet.IP.String() + } + } + t.Fatal("no non-loopback IPv4 address found") + return "" +} + +type PAMTestInfra struct { + Infisical *helpers.InfisicalService + ApiClient client.ClientWithResponsesInterface + Identity helpers.MachineIdentity + ProjectId string + GatewayId openapitypes.UUID + RelayCmd *helpers.Command + GatewayCmd *helpers.Command + ProvisionResult *client.ProvisionResult + SharedHomeDir string +} + +func SetupPAMInfra(t *testing.T, ctx context.Context) *PAMTestInfra { + infisical := helpers.NewInfisicalService(). + WithBackendEnvironment(types.NewMappingWithEquals([]string{ + "ALLOW_INTERNAL_IP_CONNECTIONS=true", + })). + Up(t, ctx) + + c := infisical.ApiClient() + identity := infisical.CreateMachineIdentity(t, ctx, helpers.WithTokenAuth()) + require.NotNil(t, identity) + + // Start relay. + // Use the host's outbound IP so the pam db access subprocess (which runs + // on the host) can resolve the relay address returned by the backend API. + relayHost := getOutboundIP(t) + relayName := helpers.RandomSlug(2) + relayCmd := &helpers.Command{ + Test: t, + Args: []string{"relay", "start", "--domain", infisical.ApiUrl(t)}, + Env: map[string]string{ + "INFISICAL_API_URL": infisical.ApiUrl(t), + "INFISICAL_RELAY_NAME": relayName, + "INFISICAL_RELAY_HOST": relayHost, + "INFISICAL_TOKEN": *identity.TokenAuthToken, + }, + } + relayCmd.Start(ctx) + t.Cleanup(relayCmd.Stop) + result := helpers.WaitForStderr(t, helpers.WaitForStderrOptions{ + EnsureCmdRunning: relayCmd, + ExpectedString: "Relay is reachable by Infisical", + }) + require.Equal(t, helpers.WaitSuccess, result) + + // Start gateway + tmpLogDir := t.TempDir() + sessionRecordingPath := filepath.Join(tmpLogDir, "session-recording") + require.NoError(t, os.MkdirAll(sessionRecordingPath, 0755)) + gatewayName := helpers.RandomSlug(2) + gatewayCmd := &helpers.Command{ + Test: t, + Args: []string{"gateway", "start", + fmt.Sprintf("--name=%s", gatewayName), + fmt.Sprintf("--pam-session-recording-path=%s", sessionRecordingPath), + }, + Env: map[string]string{ + "INFISICAL_API_URL": infisical.ApiUrl(t), + "INFISICAL_TOKEN": *identity.TokenAuthToken, + }, + } + gatewayCmd.Start(ctx) + t.Cleanup(gatewayCmd.Stop) + result = helpers.WaitForStderr(t, helpers.WaitForStderrOptions{ + EnsureCmdRunning: gatewayCmd, + ExpectedString: "Gateway is reachable by Infisical", + }) + require.Equal(t, helpers.WaitSuccess, result) + + // Find gateway ID + var gatewayId openapitypes.UUID + resp, err := c.ListGatewaysWithResponse(ctx) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode()) + for _, gateway := range *resp.JSON200 { + if gateway.Name == gatewayName && gateway.Heartbeat != nil { + gatewayId = gateway.Id + slog.Info("Found gateway ID", "gatewayId", gatewayId) + break + } + } + require.NotZero(t, gatewayId, "Gateway ID should be set") + + // Create PAM project + projDesc := "e2e tests for PAM postgres" + template := "default" + projectType := client.Pam + projectResp, err := c.CreateProjectWithResponse(ctx, client.CreateProjectJSONRequestBody{ + ProjectName: "pam-pg-tests", + ProjectDescription: &projDesc, + Template: &template, + Type: &projectType, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, projectResp.StatusCode()) + projectId := projectResp.JSON200.Project.Id + + // Create shared HOME dir for login and pam commands. + // Pre-seed the config to use file-based vault so the CLI never + // attempts to access the system keychain. + sharedHomeDir := t.TempDir() + infisicalConfigDir := filepath.Join(sharedHomeDir, ".infisical") + require.NoError(t, os.MkdirAll(infisicalConfigDir, 0755)) + configData, err := json.Marshal(map[string]string{ + "vaultBackendType": "file", + "vaultBackendPassphrase": base64.StdEncoding.EncodeToString([]byte("e2e-test-passphrase")), + }) + require.NoError(t, err) + require.NoError(t, os.WriteFile( + filepath.Join(infisicalConfigDir, "infisical-config.json"), + configData, 0644, + )) + + return &PAMTestInfra{ + Infisical: infisical, + ApiClient: c, + Identity: identity, + ProjectId: projectId, + GatewayId: gatewayId, + RelayCmd: relayCmd, + GatewayCmd: gatewayCmd, + ProvisionResult: infisical.ProvisionResult(), + SharedHomeDir: sharedHomeDir, + } +} + +func LoginUser(t *testing.T, ctx context.Context, infra *PAMTestInfra) { + loginCmd := helpers.Command{ + Test: t, + RunMethod: helpers.RunMethodSubprocess, + DisableTempHomeDir: true, + Args: []string{ + "login", + "--email", infra.ProvisionResult.Email, + "--password", infra.ProvisionResult.Password, + "--organization-id", infra.ProvisionResult.OrgId, + "--domain", infra.Infisical.ApiUrl(t), + }, + Env: map[string]string{ + "HOME": infra.SharedHomeDir, + "INFISICAL_API_URL": infra.Infisical.ApiUrl(t), + }, + } + loginCmd.Start(ctx) + + // Login is a short-lived command that exits on completion. + // Do NOT use EnsureCmdRunning — it treats any exit as failure. + result := helpers.WaitFor(t, helpers.WaitForOptions{ + Condition: func() helpers.ConditionResult { + if !loginCmd.IsRunning() { + if loginCmd.ExitCode() == 0 { + slog.Info("Login completed successfully") + return helpers.ConditionSuccess + } + loginCmd.DumpOutput() + return helpers.ConditionBreakEarly + } + return helpers.ConditionWait + }, + }) + require.Equal(t, helpers.WaitSuccess, result, "Login should succeed") +} diff --git a/e2e/pam/postgres_test.go b/e2e/pam/postgres_test.go new file mode 100644 index 0000000..9ff752d --- /dev/null +++ b/e2e/pam/postgres_test.go @@ -0,0 +1,183 @@ +package pam_test + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/infisical/cli/e2e-tests/packages/client" + helpers "github.com/infisical/cli/e2e-tests/util" + "github.com/jackc/pgx/v5" + "github.com/joho/godotenv" + "github.com/stretchr/testify/require" + tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +func TestMain(m *testing.M) { + _ = godotenv.Load("../.env") + os.Exit(m.Run()) +} + +func TestPAM_Postgres_ConnectToDatabase(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + infra := SetupPAMInfra(t, ctx) + + const ( + pgUser = "pamuser" + pgPassword = "pampassword" + pgDatabase = "testdb" + ) + + // Start PAM-target Postgres via testcontainers + pgContainer, err := tcpostgres.Run(ctx, "postgres:16", + tcpostgres.WithDatabase(pgDatabase), + tcpostgres.WithUsername(pgUser), + tcpostgres.WithPassword(pgPassword), + tcpostgres.BasicWaitStrategies(), + ) + require.NoError(t, err) + t.Cleanup(func() { + if err := pgContainer.Terminate(ctx); err != nil { + t.Logf("Failed to terminate Postgres container: %v", err) + } + }) + + // Verify PAM Postgres is reachable directly + pgConnStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + directConn, err := pgx.Connect(ctx, pgConnStr) + require.NoError(t, err) + var directResult int + err = directConn.QueryRow(ctx, "SELECT 1").Scan(&directResult) + require.NoError(t, err) + require.Equal(t, 1, directResult) + directConn.Close(ctx) + slog.Info("Verified PAM Postgres is accessible directly") + + // Get host/port for PAM resource creation + pgHost, err := pgContainer.Host(ctx) + require.NoError(t, err) + pgPort, err := pgContainer.MappedPort(ctx, "5432") + require.NoError(t, err) + + // Create Postgres PAM resource via API + resourceName := "pg-resource" + pgResResp, err := infra.ApiClient.CreatePostgresPamResourceWithResponse( + ctx, + client.CreatePostgresPamResourceJSONRequestBody{ + ProjectId: uuid.MustParse(infra.ProjectId), + GatewayId: infra.GatewayId, + Name: resourceName, + ConnectionDetails: struct { + Database string `json:"database"` + Host string `json:"host"` + Port float32 `json:"port"` + SslCertificate *string `json:"sslCertificate,omitempty"` + SslEnabled bool `json:"sslEnabled"` + SslRejectUnauthorized bool `json:"sslRejectUnauthorized"` + }{ + Host: pgHost, + Port: float32(pgPort.Int()), + Database: pgDatabase, + SslEnabled: false, + SslRejectUnauthorized: false, + }, + }, + ) + require.NoError(t, err) + require.Equal(t, http.StatusOK, pgResResp.StatusCode()) + resourceId := pgResResp.JSON200.Resource.Id + slog.Info("Created Postgres PAM resource", "resourceId", resourceId) + + // Create Postgres PAM account via API + accountName := "pg-account" + pgAcctResp, err := infra.ApiClient.CreatePostgresPamAccountWithResponse( + ctx, + client.CreatePostgresPamAccountJSONRequestBody{ + ResourceId: resourceId, + Name: accountName, + RotationEnabled: false, + Credentials: struct { + Password string `json:"password"` + Username string `json:"username"` + }{ + Username: pgUser, + Password: pgPassword, + }, + }, + ) + require.NoError(t, err) + require.Equal(t, http.StatusOK, pgAcctResp.StatusCode()) + slog.Info("Created Postgres PAM account") + + // Login with provisioned admin user + LoginUser(t, ctx, infra) + + // Run pam db access + freePort := helpers.GetFreePort() + pamCmd := helpers.Command{ + Test: t, + RunMethod: helpers.RunMethodSubprocess, + DisableTempHomeDir: true, + Args: []string{ + "pam", "db", "access", + "--resource", resourceName, + "--account", accountName, + "--project-id", infra.ProjectId, + "--duration", "5m", + "--port", fmt.Sprintf("%d", freePort), + }, + Env: map[string]string{ + "HOME": infra.SharedHomeDir, + "INFISICAL_API_URL": infra.Infisical.ApiUrl(t), + }, + } + pamCmd.Start(ctx) + t.Cleanup(pamCmd.Stop) + + // Wait for proxy to be ready (printed to stdout via fmt.Printf) + result := helpers.WaitFor(t, helpers.WaitForOptions{ + EnsureCmdRunning: &pamCmd, + Condition: func() helpers.ConditionResult { + if strings.Contains(pamCmd.Stdout(), "Database Proxy Session Started") { + return helpers.ConditionSuccess + } + return helpers.ConditionWait + }, + }) + require.Equal(t, helpers.WaitSuccess, result, "Database proxy should start successfully") + + // Connect via pgx to the proxy and run SELECT 1 + proxyConnStr := fmt.Sprintf("postgres://%s@localhost:%d/%s?sslmode=disable", pgUser, freePort, pgDatabase) + var proxyConn *pgx.Conn + connectResult := helpers.WaitFor(t, helpers.WaitForOptions{ + EnsureCmdRunning: &pamCmd, + Interval: 2 * time.Second, + Timeout: 30 * time.Second, + Condition: func() helpers.ConditionResult { + conn, err := pgx.Connect(ctx, proxyConnStr) + if err != nil { + slog.Warn("Proxy connection attempt failed, retrying...", "error", err) + return helpers.ConditionWait + } + proxyConn = conn + return helpers.ConditionSuccess + }, + }) + require.Equal(t, helpers.WaitSuccess, connectResult, "Should connect to database through proxy") + defer proxyConn.Close(ctx) + + var proxyResult int + err = proxyConn.QueryRow(ctx, "SELECT 1").Scan(&proxyResult) + require.NoError(t, err) + require.Equal(t, 1, proxyResult) + slog.Info("SELECT 1 through PAM proxy succeeded") +} diff --git a/e2e/util/helpers.go b/e2e/util/helpers.go index 271705d..f45cf74 100644 --- a/e2e/util/helpers.go +++ b/e2e/util/helpers.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "strings" "sync" "testing" @@ -257,19 +258,62 @@ type Command struct { functionCallErrMu sync.Mutex } +// findModuleRoot walks up from the current directory to find the directory +// containing go.mod, which is the e2e module root. Returns "" if not found. +func findModuleRoot() string { + dir, err := os.Getwd() + if err != nil { + return "" + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + return "" + } + dir = parent + } +} + +// resolveExecutablePath resolves a potentially relative executable path. +// Since go test sets the working directory to the test package directory +// (e.g. e2e/pam/), but .env paths are relative to the e2e module root, +// we try the path as-is first, then resolve it relative to the module root. +func resolveExecutablePath(execPath string) string { + if filepath.IsAbs(execPath) { + return execPath + } + // Try as-is from current working directory + if validateExecutable(execPath) == nil { + return execPath + } + // Try relative to the module root (e2e/) + if root := findModuleRoot(); root != "" { + candidate := filepath.Join(root, execPath) + if validateExecutable(candidate) == nil { + return candidate + } + } + // Return original path so the caller gets a meaningful error + return execPath +} + func findExecutable(t *testing.T) string { // First, check for INFISICAL_CLI_EXECUTABLE environment variable envExec := os.Getenv("INFISICAL_CLI_EXECUTABLE") if envExec != "" { - if err := validateExecutable(envExec); err != nil { + resolved := resolveExecutablePath(envExec) + if err := validateExecutable(resolved); err != nil { t.Fatalf("INFISICAL_CLI_EXECUTABLE is set to '%s' but the executable cannot be found or is not executable: %v\n"+ "Please ensure the path is correct and the file has execute permissions.", envExec, err) } - return envExec + return resolved } // Fall back to default path - defaultPath := "./infisical-merge" + defaultPath := resolveExecutablePath("./infisical-merge") if err := validateExecutable(defaultPath); err != nil { t.Fatalf("Cannot find executable at default path '%s': %v\n"+ "Please either:\n"+