diff --git a/e2e/README.md b/e2e/README.md index 7b84acc..5ec7cf0 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -176,6 +176,37 @@ 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. +### Running PAM tests + +The PAM tests validate end-to-end access through the PAM system. Each test spins up the required Docker containers, creates PAM resources and accounts, then verifies connectivity through the CLI. + +To run all PAM tests: + +```bash +cd e2e +go test -v -timeout 30m -count=1 github.com/infisical/cli/e2e-tests/pam +``` + +To run a specific test (e.g., only SSH or only Postgres): + +```bash +cd e2e +go test -v -timeout 30m -count=1 -run TestPAM_SSH github.com/infisical/cli/e2e-tests/pam +go test -v -timeout 30m -count=1 -run TestPAM_Postgres github.com/infisical/cli/e2e-tests/pam +``` + +To run a specific sub-test (e.g., only certificate auth within SSH): + +```bash +cd e2e +go test -v -timeout 30m -count=1 -run TestPAM_SSH/certificate github.com/infisical/cli/e2e-tests/pam +``` + +**Prerequisites:** +- Docker must be running (tests use testcontainers to start resource containers) +- A built CLI binary (see note above) +- `INFISICAL_BACKEND_DIR` must be set (see [Setting the `INFISICAL_BACKEND_DIR` value](#setting-the-infisical_backend_dir-value)) + Alternatively, you can export environment variables manually: ```bash diff --git a/e2e/go.mod b/e2e/go.mod index 0d4f479..4c13b03 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -22,6 +22,8 @@ require ( 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 + golang.org/x/crypto v0.45.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -298,7 +300,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect @@ -319,7 +320,6 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.3 // indirect k8s.io/apimachinery v0.32.3 // indirect k8s.io/client-go v0.32.3 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index cc33a8d..1c7eac9 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -1079,8 +1079,8 @@ golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/e2e/openapi-cfg.yaml b/e2e/openapi-cfg.yaml index 2cbd9bb..ebb9f44 100644 --- a/e2e/openapi-cfg.yaml +++ b/e2e/openapi-cfg.yaml @@ -34,3 +34,5 @@ output-options: - createCloudflareAppConnection - createPostgresPamResource - createPostgresPamAccount + - createSshPamResource + - createSshPamAccount diff --git a/e2e/packages/client/client.gen.go b/e2e/packages/client/client.gen.go index c63c2a9..97cdfd9 100644 --- a/e2e/packages/client/client.gen.go +++ b/e2e/packages/client/client.gen.go @@ -229,11 +229,41 @@ const ( SelfSigned CreateCertificateProfileJSONBodyIssuerType = "self-signed" ) +// Defines values for CreateSshPamAccountJSONBodyCredentials0AuthMethod. +const ( + CreateSshPamAccountJSONBodyCredentials0AuthMethodPassword CreateSshPamAccountJSONBodyCredentials0AuthMethod = "password" +) + +// Defines values for CreateSshPamAccountJSONBodyCredentials1AuthMethod. +const ( + CreateSshPamAccountJSONBodyCredentials1AuthMethodPublicKey CreateSshPamAccountJSONBodyCredentials1AuthMethod = "public-key" +) + +// Defines values for CreateSshPamAccountJSONBodyCredentials2AuthMethod. +const ( + CreateSshPamAccountJSONBodyCredentials2AuthMethodCertificate CreateSshPamAccountJSONBodyCredentials2AuthMethod = "certificate" +) + // Defines values for CreateKubernetesPamResourceJSONBodyRotationAccountCredentials0AuthMethod. const ( ServiceAccountToken CreateKubernetesPamResourceJSONBodyRotationAccountCredentials0AuthMethod = "service-account-token" ) +// Defines values for CreateSshPamResourceJSONBodyRotationAccountCredentials0AuthMethod. +const ( + CreateSshPamResourceJSONBodyRotationAccountCredentials0AuthMethodPassword CreateSshPamResourceJSONBodyRotationAccountCredentials0AuthMethod = "password" +) + +// Defines values for CreateSshPamResourceJSONBodyRotationAccountCredentials1AuthMethod. +const ( + CreateSshPamResourceJSONBodyRotationAccountCredentials1AuthMethodPublicKey CreateSshPamResourceJSONBodyRotationAccountCredentials1AuthMethod = "public-key" +) + +// Defines values for CreateSshPamResourceJSONBodyRotationAccountCredentials2AuthMethod. +const ( + CreateSshPamResourceJSONBodyRotationAccountCredentials2AuthMethodCertificate CreateSshPamResourceJSONBodyRotationAccountCredentials2AuthMethod = "certificate" +) + // Defines values for CreateProjectJSONBodyType. const ( Ai CreateProjectJSONBodyType = "ai" @@ -764,6 +794,56 @@ type CreatePostgresPamAccountJSONBody struct { RotationIntervalSeconds *float32 `json:"rotationIntervalSeconds"` } +// CreateSshPamAccountJSONBody defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBody struct { + Credentials CreateSshPamAccountJSONBody_Credentials `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"` +} + +// CreateSshPamAccountJSONBodyCredentials0 defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBodyCredentials0 struct { + AuthMethod CreateSshPamAccountJSONBodyCredentials0AuthMethod `json:"authMethod"` + Password string `json:"password"` + Username string `json:"username"` +} + +// CreateSshPamAccountJSONBodyCredentials0AuthMethod defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBodyCredentials0AuthMethod string + +// CreateSshPamAccountJSONBodyCredentials1 defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBodyCredentials1 struct { + AuthMethod CreateSshPamAccountJSONBodyCredentials1AuthMethod `json:"authMethod"` + PrivateKey string `json:"privateKey"` + Username string `json:"username"` +} + +// CreateSshPamAccountJSONBodyCredentials1AuthMethod defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBodyCredentials1AuthMethod string + +// CreateSshPamAccountJSONBodyCredentials2 defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBodyCredentials2 struct { + AuthMethod CreateSshPamAccountJSONBodyCredentials2AuthMethod `json:"authMethod"` + Username string `json:"username"` +} + +// CreateSshPamAccountJSONBodyCredentials2AuthMethod defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBodyCredentials2AuthMethod string + +// CreateSshPamAccountJSONBody_Credentials defines parameters for CreateSshPamAccount. +type CreateSshPamAccountJSONBody_Credentials struct { + union json.RawMessage +} + // CreateKubernetesPamResourceJSONBody defines parameters for CreateKubernetesPamResource. type CreateKubernetesPamResourceJSONBody struct { ConnectionDetails struct { @@ -840,6 +920,56 @@ type CreateRedisPamResourceJSONBody struct { } `json:"rotationAccountCredentials"` } +// CreateSshPamResourceJSONBody defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBody struct { + ConnectionDetails struct { + Host string `json:"host"` + Port float32 `json:"port"` + } `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 *CreateSshPamResourceJSONBody_RotationAccountCredentials `json:"rotationAccountCredentials"` +} + +// CreateSshPamResourceJSONBodyRotationAccountCredentials0 defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBodyRotationAccountCredentials0 struct { + AuthMethod CreateSshPamResourceJSONBodyRotationAccountCredentials0AuthMethod `json:"authMethod"` + Password string `json:"password"` + Username string `json:"username"` +} + +// CreateSshPamResourceJSONBodyRotationAccountCredentials0AuthMethod defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBodyRotationAccountCredentials0AuthMethod string + +// CreateSshPamResourceJSONBodyRotationAccountCredentials1 defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBodyRotationAccountCredentials1 struct { + AuthMethod CreateSshPamResourceJSONBodyRotationAccountCredentials1AuthMethod `json:"authMethod"` + PrivateKey string `json:"privateKey"` + Username string `json:"username"` +} + +// CreateSshPamResourceJSONBodyRotationAccountCredentials1AuthMethod defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBodyRotationAccountCredentials1AuthMethod string + +// CreateSshPamResourceJSONBodyRotationAccountCredentials2 defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBodyRotationAccountCredentials2 struct { + AuthMethod CreateSshPamResourceJSONBodyRotationAccountCredentials2AuthMethod `json:"authMethod"` + Username string `json:"username"` +} + +// CreateSshPamResourceJSONBodyRotationAccountCredentials2AuthMethod defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBodyRotationAccountCredentials2AuthMethod string + +// CreateSshPamResourceJSONBody_RotationAccountCredentials defines parameters for CreateSshPamResource. +type CreateSshPamResourceJSONBody_RotationAccountCredentials struct { + union json.RawMessage +} + // CreateProjectJSONBody defines parameters for CreateProject. type CreateProjectJSONBody struct { HasDeleteProtection *bool `json:"hasDeleteProtection,omitempty"` @@ -1105,6 +1235,9 @@ type CreateMachineIdentityJSONRequestBody CreateMachineIdentityJSONBody // CreatePostgresPamAccountJSONRequestBody defines body for CreatePostgresPamAccount for application/json ContentType. type CreatePostgresPamAccountJSONRequestBody CreatePostgresPamAccountJSONBody +// CreateSshPamAccountJSONRequestBody defines body for CreateSshPamAccount for application/json ContentType. +type CreateSshPamAccountJSONRequestBody CreateSshPamAccountJSONBody + // CreateKubernetesPamResourceJSONRequestBody defines body for CreateKubernetesPamResource for application/json ContentType. type CreateKubernetesPamResourceJSONRequestBody CreateKubernetesPamResourceJSONBody @@ -1114,6 +1247,9 @@ type CreatePostgresPamResourceJSONRequestBody CreatePostgresPamResourceJSONBody // CreateRedisPamResourceJSONRequestBody defines body for CreateRedisPamResource for application/json ContentType. type CreateRedisPamResourceJSONRequestBody CreateRedisPamResourceJSONBody +// CreateSshPamResourceJSONRequestBody defines body for CreateSshPamResource for application/json ContentType. +type CreateSshPamResourceJSONRequestBody CreateSshPamResourceJSONBody + // CreateProjectJSONRequestBody defines body for CreateProject for application/json ContentType. type CreateProjectJSONRequestBody CreateProjectJSONBody @@ -1270,6 +1406,11 @@ type ClientInterface interface { CreatePostgresPamAccount(ctx context.Context, body CreatePostgresPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateSshPamAccountWithBody request with any body + CreateSshPamAccountWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateSshPamAccount(ctx context.Context, body CreateSshPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateKubernetesPamResourceWithBody request with any body CreateKubernetesPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1285,6 +1426,11 @@ type ClientInterface interface { CreateRedisPamResource(ctx context.Context, body CreateRedisPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateSshPamResourceWithBody request with any body + CreateSshPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateSshPamResource(ctx context.Context, body CreateSshPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateProjectWithBody request with any body CreateProjectWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1647,6 +1793,30 @@ func (c *Client) CreatePostgresPamAccount(ctx context.Context, body CreatePostgr return c.Client.Do(req) } +func (c *Client) CreateSshPamAccountWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateSshPamAccountRequestWithBody(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) CreateSshPamAccount(ctx context.Context, body CreateSshPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateSshPamAccountRequest(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 { @@ -1719,6 +1889,30 @@ func (c *Client) CreateRedisPamResource(ctx context.Context, body CreateRedisPam return c.Client.Do(req) } +func (c *Client) CreateSshPamResourceWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateSshPamResourceRequestWithBody(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) CreateSshPamResource(ctx context.Context, body CreateSshPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateSshPamResourceRequest(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) CreateProjectWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewCreateProjectRequestWithBody(c.Server, contentType, body) if err != nil { @@ -2469,6 +2663,46 @@ func NewCreatePostgresPamAccountRequestWithBody(server string, contentType strin return req, nil } +// NewCreateSshPamAccountRequest calls the generic CreateSshPamAccount builder with application/json body +func NewCreateSshPamAccountRequest(server string, body CreateSshPamAccountJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateSshPamAccountRequestWithBody(server, "application/json", bodyReader) +} + +// NewCreateSshPamAccountRequestWithBody generates requests for CreateSshPamAccount with any type of body +func NewCreateSshPamAccountRequestWithBody(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/ssh") + 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 @@ -2589,6 +2823,46 @@ func NewCreateRedisPamResourceRequestWithBody(server string, contentType string, return req, nil } +// NewCreateSshPamResourceRequest calls the generic CreateSshPamResource builder with application/json body +func NewCreateSshPamResourceRequest(server string, body CreateSshPamResourceJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateSshPamResourceRequestWithBody(server, "application/json", bodyReader) +} + +// NewCreateSshPamResourceRequestWithBody generates requests for CreateSshPamResource with any type of body +func NewCreateSshPamResourceRequestWithBody(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/ssh") + 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 +} + // NewCreateProjectRequest calls the generic CreateProject builder with application/json body func NewCreateProjectRequest(server string, body CreateProjectJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -3332,6 +3606,11 @@ type ClientWithResponsesInterface interface { CreatePostgresPamAccountWithResponse(ctx context.Context, body CreatePostgresPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePostgresPamAccountResponse, error) + // CreateSshPamAccountWithBodyWithResponse request with any body + CreateSshPamAccountWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSshPamAccountResponse, error) + + CreateSshPamAccountWithResponse(ctx context.Context, body CreateSshPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateSshPamAccountResponse, error) + // CreateKubernetesPamResourceWithBodyWithResponse request with any body CreateKubernetesPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateKubernetesPamResourceResponse, error) @@ -3347,6 +3626,11 @@ type ClientWithResponsesInterface interface { CreateRedisPamResourceWithResponse(ctx context.Context, body CreateRedisPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateRedisPamResourceResponse, error) + // CreateSshPamResourceWithBodyWithResponse request with any body + CreateSshPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSshPamResourceResponse, error) + + CreateSshPamResourceWithResponse(ctx context.Context, body CreateSshPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateSshPamResourceResponse, error) + // CreateProjectWithBodyWithResponse request with any body CreateProjectWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateProjectResponse, error) @@ -4744,6 +5028,124 @@ func (r CreatePostgresPamAccountResponse) StatusCode() int { return 0 } +type CreateSshPamAccountResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Account struct { + CreatedAt time.Time `json:"createdAt"` + Credentials CreateSshPamAccount_200_Account_Credentials `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 CreateSshPamAccount200AccountResourceType `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 CreateSshPamAccount400StatusCode `json:"statusCode"` + } + JSON401 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamAccount401StatusCode `json:"statusCode"` + } + JSON403 *struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamAccount403StatusCode `json:"statusCode"` + } + JSON404 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamAccount404StatusCode `json:"statusCode"` + } + JSON422 *struct { + Error string `json:"error"` + Message interface{} `json:"message,omitempty"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamAccount422StatusCode `json:"statusCode"` + } + JSON500 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamAccount500StatusCode `json:"statusCode"` + } +} +type CreateSshPamAccount200AccountCredentials0 struct { + AuthMethod CreateSshPamAccount200AccountCredentials0AuthMethod `json:"authMethod"` + Username string `json:"username"` +} +type CreateSshPamAccount200AccountCredentials0AuthMethod string +type CreateSshPamAccount200AccountCredentials1 struct { + AuthMethod CreateSshPamAccount200AccountCredentials1AuthMethod `json:"authMethod"` + Username string `json:"username"` +} +type CreateSshPamAccount200AccountCredentials1AuthMethod string +type CreateSshPamAccount200AccountCredentials2 struct { + AuthMethod CreateSshPamAccount200AccountCredentials2AuthMethod `json:"authMethod"` + Username string `json:"username"` +} +type CreateSshPamAccount200AccountCredentials2AuthMethod string +type CreateSshPamAccount_200_Account_Credentials struct { + union json.RawMessage +} +type CreateSshPamAccount200AccountResourceType string +type CreateSshPamAccount400StatusCode float32 +type CreateSshPamAccount401StatusCode float32 +type CreateSshPamAccount403StatusCode float32 +type CreateSshPamAccount404StatusCode float32 +type CreateSshPamAccount422StatusCode float32 +type CreateSshPamAccount500StatusCode float32 + +// Status returns HTTPResponse.Status +func (r CreateSshPamAccountResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateSshPamAccountResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type CreateKubernetesPamResourceResponse struct { Body []byte HTTPResponse *http.Response @@ -5033,12 +5435,120 @@ func (r CreateRedisPamResourceResponse) StatusCode() int { return 0 } -type CreateProjectResponse struct { +type CreateSshPamResourceResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - Project struct { - UnderscoreId string `json:"_id"` + Resource struct { + AdServerResourceId *openapi_types.UUID `json:"adServerResourceId"` + ConnectionDetails struct { + Host string `json:"host"` + Port float32 `json:"port"` + } `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 CreateSshPamResource200ResourceResourceType `json:"resourceType"` + RotationAccountCredentials *CreateSshPamResource_200_Resource_RotationAccountCredentials `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 CreateSshPamResource400StatusCode `json:"statusCode"` + } + JSON401 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamResource401StatusCode `json:"statusCode"` + } + JSON403 *struct { + Details interface{} `json:"details,omitempty"` + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamResource403StatusCode `json:"statusCode"` + } + JSON404 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamResource404StatusCode `json:"statusCode"` + } + JSON422 *struct { + Error string `json:"error"` + Message interface{} `json:"message,omitempty"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamResource422StatusCode `json:"statusCode"` + } + JSON500 *struct { + Error string `json:"error"` + Message string `json:"message"` + ReqId string `json:"reqId"` + StatusCode CreateSshPamResource500StatusCode `json:"statusCode"` + } +} +type CreateSshPamResource200ResourceResourceType string +type CreateSshPamResource200ResourceRotationAccountCredentials0 struct { + AuthMethod CreateSshPamResource200ResourceRotationAccountCredentials0AuthMethod `json:"authMethod"` + Username string `json:"username"` +} +type CreateSshPamResource200ResourceRotationAccountCredentials0AuthMethod string +type CreateSshPamResource200ResourceRotationAccountCredentials1 struct { + AuthMethod CreateSshPamResource200ResourceRotationAccountCredentials1AuthMethod `json:"authMethod"` + Username string `json:"username"` +} +type CreateSshPamResource200ResourceRotationAccountCredentials1AuthMethod string +type CreateSshPamResource200ResourceRotationAccountCredentials2 struct { + AuthMethod CreateSshPamResource200ResourceRotationAccountCredentials2AuthMethod `json:"authMethod"` + Username string `json:"username"` +} +type CreateSshPamResource200ResourceRotationAccountCredentials2AuthMethod string +type CreateSshPamResource_200_Resource_RotationAccountCredentials struct { + union json.RawMessage +} +type CreateSshPamResource400StatusCode float32 +type CreateSshPamResource401StatusCode float32 +type CreateSshPamResource403StatusCode float32 +type CreateSshPamResource404StatusCode float32 +type CreateSshPamResource422StatusCode float32 +type CreateSshPamResource500StatusCode float32 + +// Status returns HTTPResponse.Status +func (r CreateSshPamResourceResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateSshPamResourceResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CreateProjectResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *struct { + Project struct { + UnderscoreId string `json:"_id"` AuditLogsRetentionDays *float32 `json:"auditLogsRetentionDays"` AutoCapitalization *bool `json:"autoCapitalization"` CreatedAt time.Time `json:"createdAt"` @@ -6163,6 +6673,23 @@ func (c *ClientWithResponses) CreatePostgresPamAccountWithResponse(ctx context.C return ParseCreatePostgresPamAccountResponse(rsp) } +// CreateSshPamAccountWithBodyWithResponse request with arbitrary body returning *CreateSshPamAccountResponse +func (c *ClientWithResponses) CreateSshPamAccountWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSshPamAccountResponse, error) { + rsp, err := c.CreateSshPamAccountWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateSshPamAccountResponse(rsp) +} + +func (c *ClientWithResponses) CreateSshPamAccountWithResponse(ctx context.Context, body CreateSshPamAccountJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateSshPamAccountResponse, error) { + rsp, err := c.CreateSshPamAccount(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateSshPamAccountResponse(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...) @@ -6214,6 +6741,23 @@ func (c *ClientWithResponses) CreateRedisPamResourceWithResponse(ctx context.Con return ParseCreateRedisPamResourceResponse(rsp) } +// CreateSshPamResourceWithBodyWithResponse request with arbitrary body returning *CreateSshPamResourceResponse +func (c *ClientWithResponses) CreateSshPamResourceWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateSshPamResourceResponse, error) { + rsp, err := c.CreateSshPamResourceWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateSshPamResourceResponse(rsp) +} + +func (c *ClientWithResponses) CreateSshPamResourceWithResponse(ctx context.Context, body CreateSshPamResourceJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateSshPamResourceResponse, error) { + rsp, err := c.CreateSshPamResource(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateSshPamResourceResponse(rsp) +} + // CreateProjectWithBodyWithResponse request with arbitrary body returning *CreateProjectResponse func (c *ClientWithResponses) CreateProjectWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateProjectResponse, error) { rsp, err := c.CreateProjectWithBody(ctx, contentType, body, reqEditors...) @@ -8106,6 +8650,139 @@ func ParseCreatePostgresPamAccountResponse(rsp *http.Response) (*CreatePostgresP return response, nil } +// ParseCreateSshPamAccountResponse parses an HTTP response from a CreateSshPamAccountWithResponse call +func ParseCreateSshPamAccountResponse(rsp *http.Response) (*CreateSshPamAccountResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateSshPamAccountResponse{ + 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 CreateSshPamAccount_200_Account_Credentials `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 CreateSshPamAccount200AccountResourceType `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 CreateSshPamAccount400StatusCode `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 CreateSshPamAccount401StatusCode `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 CreateSshPamAccount403StatusCode `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 CreateSshPamAccount404StatusCode `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 CreateSshPamAccount422StatusCode `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 CreateSshPamAccount500StatusCode `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) @@ -8487,6 +9164,129 @@ func ParseCreateRedisPamResourceResponse(rsp *http.Response) (*CreateRedisPamRes return response, nil } +// ParseCreateSshPamResourceResponse parses an HTTP response from a CreateSshPamResourceWithResponse call +func ParseCreateSshPamResourceResponse(rsp *http.Response) (*CreateSshPamResourceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateSshPamResourceResponse{ + 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 { + Host string `json:"host"` + Port float32 `json:"port"` + } `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 CreateSshPamResource200ResourceResourceType `json:"resourceType"` + RotationAccountCredentials *CreateSshPamResource_200_Resource_RotationAccountCredentials `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 CreateSshPamResource400StatusCode `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 CreateSshPamResource401StatusCode `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 CreateSshPamResource403StatusCode `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 CreateSshPamResource404StatusCode `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 CreateSshPamResource422StatusCode `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 CreateSshPamResource500StatusCode `json:"statusCode"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseCreateProjectResponse parses an HTTP response from a CreateProjectWithResponse call func ParseCreateProjectResponse(rsp *http.Response) (*CreateProjectResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/e2e/pam/main_test.go b/e2e/pam/main_test.go new file mode 100644 index 0000000..d4e4e0f --- /dev/null +++ b/e2e/pam/main_test.go @@ -0,0 +1,13 @@ +package pam + +import ( + "os" + "testing" + + "github.com/joho/godotenv" +) + +func TestMain(m *testing.M) { + _ = godotenv.Load("../.env") + os.Exit(m.Run()) +} diff --git a/e2e/pam/pam_helpers.go b/e2e/pam/pam_helpers.go index c00d97d..1f7b249 100644 --- a/e2e/pam/pam_helpers.go +++ b/e2e/pam/pam_helpers.go @@ -1,4 +1,4 @@ -package pam_test +package pam import ( "context" @@ -60,7 +60,7 @@ func SetupPAMInfra(t *testing.T, ctx context.Context) *PAMTestInfra { require.NotNil(t, identity) // Start relay. - // Use the host's outbound IP so the pam db access subprocess (which runs + // Use the host's outbound IP so the pam access subprocess (which runs // on the host) can resolve the relay address returned by the backend API. relayHost := getOutboundIP(t) relayName := helpers.RandomSlug(2) @@ -121,11 +121,11 @@ func SetupPAMInfra(t *testing.T, ctx context.Context) *PAMTestInfra { require.NotZero(t, gatewayId, "Gateway ID should be set") // Create PAM project - projDesc := "e2e tests for PAM postgres" + projDesc := "e2e tests for PAM" template := "default" projectType := client.Pam projectResp, err := c.CreateProjectWithResponse(ctx, client.CreateProjectJSONRequestBody{ - ProjectName: "pam-pg-tests", + ProjectName: "pam-e2e-tests", ProjectDescription: &projDesc, Template: &template, Type: &projectType, diff --git a/e2e/pam/postgres_test.go b/e2e/pam/postgres_test.go index 9ff752d..4f67ea7 100644 --- a/e2e/pam/postgres_test.go +++ b/e2e/pam/postgres_test.go @@ -1,11 +1,10 @@ -package pam_test +package pam import ( "context" "fmt" "log/slog" "net/http" - "os" "strings" "testing" "time" @@ -14,16 +13,10 @@ import ( "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) diff --git a/e2e/pam/ssh_test.go b/e2e/pam/ssh_test.go new file mode 100644 index 0000000..4d79b1d --- /dev/null +++ b/e2e/pam/ssh_test.go @@ -0,0 +1,284 @@ +package pam + +import ( + "bytes" + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/google/uuid" + "github.com/infisical/cli/e2e-tests/packages/client" + helpers "github.com/infisical/cli/e2e-tests/util" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "golang.org/x/crypto/ssh" +) + +const ( + // Matches the hardcoded values in testdata/ssh-server/entrypoint.sh. + sshUser = "testuser" + sshPassword = "testpass" +) + +func startSSHContainer(t *testing.T, ctx context.Context, env map[string]string) (testcontainers.Container, int) { + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Context: "testdata/ssh-server", + Dockerfile: "Dockerfile", + }, + ExposedPorts: []string{"22/tcp"}, + Env: env, + HostConfigModifier: func(hc *container.HostConfig) { + hc.ExtraHosts = append(hc.ExtraHosts, "host.docker.internal:host-gateway") + }, + WaitingFor: wait.ForListeningPort("22/tcp").WithStartupTimeout(30 * time.Second), + }, + Started: true, + }) + require.NoError(t, err) + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("Failed to terminate SSH container: %v", err) + } + }) + + port, err := container.MappedPort(ctx, "22") + require.NoError(t, err) + return container, port.Int() +} + +func createSSHPamResource(t *testing.T, ctx context.Context, infra *PAMTestInfra, name, host string, port int) uuid.UUID { + resp, err := infra.ApiClient.CreateSshPamResourceWithResponse( + ctx, + client.CreateSshPamResourceJSONRequestBody{ + ProjectId: uuid.MustParse(infra.ProjectId), + GatewayId: infra.GatewayId, + Name: name, + ConnectionDetails: struct { + Host string `json:"host"` + Port float32 `json:"port"` + }{ + Host: host, + Port: float32(port), + }, + }, + ) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode()) + slog.Info("Created SSH PAM resource", "resourceId", resp.JSON200.Resource.Id, "name", name) + return resp.JSON200.Resource.Id +} + +func createSSHPamAccount(t *testing.T, ctx context.Context, infra *PAMTestInfra, resourceId uuid.UUID, name string, credentials map[string]interface{}) { + body, err := json.Marshal(map[string]interface{}{ + "resourceId": resourceId.String(), + "name": name, + "rotationEnabled": false, + "credentials": credentials, + }) + require.NoError(t, err) + + result := helpers.WaitFor(t, helpers.WaitForOptions{ + Timeout: 90 * time.Second, + Interval: 3 * time.Second, + Condition: func() helpers.ConditionResult { + resp, callErr := infra.ApiClient.CreateSshPamAccountWithBodyWithResponse( + ctx, "application/json", bytes.NewReader(append([]byte(nil), body...)), + ) + if callErr != nil { + slog.Warn("SSH PAM account creation attempt failed, retrying...", "error", callErr) + return helpers.ConditionWait + } + if resp.StatusCode() != http.StatusOK { + slog.Warn("SSH PAM account creation returned non-200, retrying...", "status", resp.StatusCode(), "body", string(resp.Body)) + return helpers.ConditionWait + } + return helpers.ConditionSuccess + }, + }) + require.Equal(t, helpers.WaitSuccess, result, "SSH PAM account creation should succeed for %s", name) + slog.Info("Created SSH PAM account", "name", name) +} + +func runSSHSessionAndVerify(t *testing.T, ctx context.Context, infra *PAMTestInfra, resourceName, accountName, command, expectedOutput string) { + stdinReader, stdinWriter := io.Pipe() + + pamCmd := helpers.Command{ + Test: t, + RunMethod: helpers.RunMethodSubprocess, + DisableTempHomeDir: true, + Stdin: stdinReader, + Args: []string{ + "pam", "ssh", "access", + "--resource", resourceName, + "--account", accountName, + "--project-id", infra.ProjectId, + "--duration", "5m", + }, + Env: map[string]string{ + "HOME": infra.SharedHomeDir, + "INFISICAL_API_URL": infra.Infisical.ApiUrl(t), + "PATH": os.Getenv("PATH"), + }, + } + pamCmd.Start(ctx) + t.Cleanup(pamCmd.Stop) + + go func() { + fmt.Fprintln(stdinWriter, command) + }() + + echoResult := helpers.WaitFor(t, helpers.WaitForOptions{ + EnsureCmdRunning: &pamCmd, + Timeout: 60 * time.Second, + Interval: 1 * time.Second, + Condition: func() helpers.ConditionResult { + if strings.Contains(pamCmd.Stdout(), expectedOutput) { + return helpers.ConditionSuccess + } + return helpers.ConditionWait + }, + }) + if echoResult != helpers.WaitSuccess { + pamCmd.DumpOutput() + } + require.Equal(t, helpers.WaitSuccess, echoResult, "Should see expected output %q", expectedOutput) + + stdinWriter.Close() + + exitResult := helpers.WaitFor(t, helpers.WaitForOptions{ + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + Condition: func() helpers.ConditionResult { + if !pamCmd.IsRunning() { + if pamCmd.ExitCode() == 0 { + slog.Info("PAM SSH access completed successfully") + return helpers.ConditionSuccess + } + pamCmd.DumpOutput() + return helpers.ConditionBreakEarly + } + return helpers.ConditionWait + }, + }) + require.Equal(t, helpers.WaitSuccess, exitResult, "pam ssh access should complete successfully") +} + +// configureCertAuth replicates the real user flow for certificate auth setup: +// the frontend shows a `curl | sudo bash` command that the user +// runs on their SSH server. We do the same inside the container. +// The setup script configures /etc/ssh but can't restart sshd (no systemctl in alpine), +// so we send SIGHUP to reload the config afterward. +func configureCertAuth(t *testing.T, ctx context.Context, infra *PAMTestInfra, container testcontainers.Container, sshPort int, resourceId uuid.UUID) { + // ApiUrl returns a localhost URL which isn't reachable from inside the container. + // Use host.docker.internal (configured via ExtraHosts in startSSHContainer) instead. + apiURL := strings.Replace(infra.Infisical.ApiUrl(t), "localhost", "host.docker.internal", 1) + setupURL := fmt.Sprintf("%s/api/v1/pam/resources/ssh/%s/ssh-ca-setup", apiURL, resourceId) + curlCmd := fmt.Sprintf(`curl -sf -H "Authorization: Bearer %s" "%s" | bash`, infra.ProvisionResult.Token, setupURL) + + exitCode, _, err := container.Exec(ctx, []string{"bash", "-c", curlCmd}) + require.NoError(t, err) + require.Equal(t, 0, exitCode, "ssh-ca-setup script should succeed") + + // The setup script can't restart sshd in alpine (no systemctl/service). + // Reload config by sending SIGHUP to sshd (PID 1). + exitCode, _, err = container.Exec(ctx, []string{"kill", "-HUP", "1"}) + require.NoError(t, err) + require.Equal(t, 0, exitCode, "sshd reload should succeed") + + // Wait for sshd to be responsive after config reload. + result := helpers.WaitFor(t, helpers.WaitForOptions{ + Timeout: 10 * time.Second, + Interval: 500 * time.Millisecond, + Condition: func() helpers.ConditionResult { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", sshPort), time.Second) + if err != nil { + return helpers.ConditionWait + } + conn.Close() + return helpers.ConditionSuccess + }, + }) + require.Equal(t, helpers.WaitSuccess, result, "sshd should be responsive after cert auth config reload") +} + +// runSSHAuthTest handles all auth-method-specific setup and runs the SSH session test. +// Each auth method configures the container and PAM account differently: +// - password: uses hardcoded testuser/testpass from entrypoint; account gets username + password +// - public-key: container gets SSH_AUTHORIZED_KEY (generated ed25519); account gets username + privateKey +// - certificate: container configured via curl | bash (ssh-ca-setup endpoint); account gets just username +func runSSHAuthTest(t *testing.T, ctx context.Context, infra *PAMTestInfra, resourceHost string, method string) { + containerEnv := map[string]string{} + accountCreds := map[string]interface{}{ + "authMethod": method, + "username": sshUser, + } + + switch method { + case "password": + accountCreds["password"] = sshPassword + + case "public-key": + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + sshPubKey, err := ssh.NewPublicKey(pubKey) + require.NoError(t, err) + containerEnv["SSH_AUTHORIZED_KEY"] = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPubKey))) + + privKeyPEM, err := ssh.MarshalPrivateKey(privKey, "") + require.NoError(t, err) + accountCreds["privateKey"] = string(pem.EncodeToMemory(privKeyPEM)) + + case "certificate": + // No extra container config needed. + // Cert auth is configured after resource creation via curl | bash. + } + + container, sshPort := startSSHContainer(t, ctx, containerEnv) + slog.Info("SSH container started", "method", method, "host", resourceHost, "port", sshPort) + + resourceName := fmt.Sprintf("ssh-%s-resource", method) + resourceId := createSSHPamResource(t, ctx, infra, resourceName, resourceHost, sshPort) + + if method == "certificate" { + configureCertAuth(t, ctx, infra, container, sshPort, resourceId) + } + + accountName := fmt.Sprintf("ssh-%s-account", method) + createSSHPamAccount(t, ctx, infra, resourceId, accountName, accountCreds) + + marker := fmt.Sprintf("hello-%s", method) + runSSHSessionAndVerify(t, ctx, infra, resourceName, accountName, "echo "+marker, marker) +} + +func TestPAM_SSH(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + infra := SetupPAMInfra(t, ctx) + LoginUser(t, ctx, infra) + + resourceHost := getOutboundIP(t) + + methods := []string{"password", "public-key", "certificate"} + for _, method := range methods { + t.Run(method, func(t *testing.T) { + runSSHAuthTest(t, ctx, infra, resourceHost, method) + }) + } +} diff --git a/e2e/pam/testdata/ssh-server/Dockerfile b/e2e/pam/testdata/ssh-server/Dockerfile new file mode 100644 index 0000000..5d24b85 --- /dev/null +++ b/e2e/pam/testdata/ssh-server/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine:3.21 + +RUN apk add --no-cache openssh-server curl bash + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 22 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/e2e/pam/testdata/ssh-server/entrypoint.sh b/e2e/pam/testdata/ssh-server/entrypoint.sh new file mode 100644 index 0000000..659e459 --- /dev/null +++ b/e2e/pam/testdata/ssh-server/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -e + +ssh-keygen -A + +adduser -D testuser +echo "testuser:testpass" | chpasswd +sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config +sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config + +if [ -n "$SSH_AUTHORIZED_KEY" ]; then + mkdir -p /home/testuser/.ssh + echo "$SSH_AUTHORIZED_KEY" > /home/testuser/.ssh/authorized_keys + chmod 700 /home/testuser/.ssh + chmod 600 /home/testuser/.ssh/authorized_keys + chown -R testuser:testuser /home/testuser/.ssh +fi + +exec /usr/sbin/sshd -D -e diff --git a/e2e/util/helpers.go b/e2e/util/helpers.go index f45cf74..fb011ad 100644 --- a/e2e/util/helpers.go +++ b/e2e/util/helpers.go @@ -243,6 +243,7 @@ type Command struct { Env map[string]string RunMethod RunMethod DisableTempHomeDir bool + Stdin io.Reader stdoutFilePath string stdoutFile *os.File @@ -417,6 +418,9 @@ func (c *Command) Start(ctx context.Context) { c.cmd.Stdout = c.stdoutFile c.cmd.Stderr = c.stderrFile + if c.Stdin != nil { + c.cmd.Stdin = c.Stdin + } err := c.cmd.Start() go func() {