diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f4bcb1..fe84694 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,5 +77,5 @@ jobs: - run: go mod download - env: TF_ACC: "1" - run: go test -v -cover ./infisical/provider/ + run: go test -v -cover ./internal/provider/ timeout-minutes: 10 diff --git a/client/helpers.go b/client/helpers.go deleted file mode 100644 index b09a5de..0000000 --- a/client/helpers.go +++ /dev/null @@ -1,134 +0,0 @@ -package infisicalclient - -import ( - "encoding/base64" - "errors" - "fmt" - "strings" -) - -type DecodedSymmetricEncryptionDetails = struct { - Cipher []byte - IV []byte - Tag []byte - Key []byte -} - -func GetPlainTextSecrets(key []byte, encryptedSecrets GetEncryptedSecretsV3Response) ([]SingleEnvironmentVariable, error) { - plainTextSecrets := []SingleEnvironmentVariable{} - for _, secret := range encryptedSecrets.Secrets { - // Decrypt key - key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV) - if err != nil { - return nil, fmt.Errorf("unable to decode secret IV for secret key") - } - - key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag) - if err != nil { - return nil, fmt.Errorf("unable to decode secret authentication tag for secret key") - } - - key_ciphertext, err := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext) - if err != nil { - return nil, fmt.Errorf("unable to decode secret cipher text for secret key") - } - - plainTextKey, err := DecryptSymmetric(key, key_ciphertext, key_tag, key_iv) - if err != nil { - return nil, fmt.Errorf("unable to symmetrically decrypt secret key") - } - - // Decrypt value - value_iv, err := base64.StdEncoding.DecodeString(secret.SecretValueIV) - if err != nil { - return nil, fmt.Errorf("unable to decode secret IV for secret value") - } - - value_tag, err := base64.StdEncoding.DecodeString(secret.SecretValueTag) - if err != nil { - return nil, fmt.Errorf("unable to decode secret authentication tag for secret value") - } - - value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext) - if err != nil { - return nil, fmt.Errorf("unable to decode secret cipher text for secret key") - } - - plainTextValue, err := DecryptSymmetric(key, value_ciphertext, value_tag, value_iv) - if err != nil { - return nil, fmt.Errorf("unable to symmetrically decrypt secret value") - } - - // Decrypt comment - comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV) - if err != nil { - return nil, fmt.Errorf("unable to decode secret IV for secret value") - } - - comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag) - if err != nil { - return nil, fmt.Errorf("unable to decode secret authentication tag for secret value") - } - - comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext) - if err != nil { - return nil, fmt.Errorf("unable to decode secret cipher text for secret key") - } - - plainTextComment, err := DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv) - if err != nil { - return nil, fmt.Errorf("unable to symmetrically decrypt secret comment") - } - - plainTextSecret := SingleEnvironmentVariable{ - Key: string(plainTextKey), - Value: string(plainTextValue), - Type: string(secret.Type), - ID: secret.ID, - Tags: secret.Tags, - Comment: string(plainTextComment), - } - - plainTextSecrets = append(plainTextSecrets, plainTextSecret) - } - - return plainTextSecrets, nil -} - -func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) { - cipherx, err := base64.StdEncoding.DecodeString(cipher) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err) - } - - keyx, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err) - } - - IVx, err := base64.StdEncoding.DecodeString(IV) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err) - } - - tagx, err := base64.StdEncoding.DecodeString(tag) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err) - } - - return DecodedSymmetricEncryptionDetails{ - Key: keyx, - Cipher: cipherx, - IV: IVx, - Tag: tagx, - }, nil -} - -func GetSymmetricKeyFromServiceToken(serviceToken string) (privateKey string, err error) { - serviceTokenParts := strings.SplitN(serviceToken, ".", 4) - if len(serviceTokenParts) < 4 { - return "", errors.New("invalid service token entered. Please double check your service token and try again") - } - - return serviceTokenParts[3], nil -} diff --git a/client/model.go b/client/model.go deleted file mode 100644 index 5c2e812..0000000 --- a/client/model.go +++ /dev/null @@ -1,295 +0,0 @@ -package infisicalclient - -import "time" - -type GetEncryptedSecretsV3Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - SecretPath string `json:"secretPath"` -} - -type EncryptedSecretV3 struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } `json:"tags"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - Algorithm string `json:"algorithm"` - KeyEncoding string `json:"keyEncoding"` - Folder string `json:"folder"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} - -type Project struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - AutoCapitalization bool `json:"autoCapitalization"` - OrgID string `json:"orgId"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - Version int `json:"version"` - - UpgradeStatus string `json:"upgradeStatus"` // can be null. if its null it will be converted to an empty string. -} - -type ProjectWithEnvironments struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - AutoCapitalization bool `json:"autoCapitalization"` - OrgID string `json:"orgId"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - Version int64 `json:"version"` - UpgradeStatus string `json:"upgradeStatus"` - Environments []ProjectEnvironment `json:"environments"` -} - -type ProjectEnvironment struct { - Name string `json:"name"` - Slug string `json:"slug"` - ID string `json:"id"` -} - -type CreateProjectResponse struct { - Project Project `json:"project"` -} - -type DeleteProjectResponse struct { - Project Project `json:"workspace"` -} - -type UpdateProjectResponse Project - -type GetEncryptedSecretsV3Response struct { - Secrets []EncryptedSecretV3 `json:"secrets"` -} - -type GetServiceTokenDetailsResponse struct { - ID string `json:"_id"` - Name string `json:"name"` - Workspace string `json:"workspace"` - Environment string `json:"environment"` - ExpiresAt time.Time `json:"expiresAt"` - EncryptedKey string `json:"encryptedKey"` - Iv string `json:"iv"` - Tag string `json:"tag"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - V int `json:"__v"` -} - -type UniversalMachineIdentityAuthResponse struct { - AccessToken string `json:"accessToken"` - ExpiresIn int `json:"expiresIn"` - AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` - TokenType string `json:"tokenType"` -} - -type SingleEnvironmentVariable struct { - Key string `json:"key"` - Value string `json:"value"` - Type string `json:"type"` - ID string `json:"_id"` - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } `json:"tags"` - Comment string `json:"comment"` -} - -type SymmetricEncryptionResult struct { - CipherText []byte `json:"CipherText"` - Nonce []byte `json:"Nonce"` - AuthTag []byte `json:"AuthTag"` -} - -// Workspace key request -type GetEncryptedWorkspaceKeyRequest struct { - WorkspaceId string `json:"workspaceId"` -} - -// Workspace key response -type GetEncryptedWorkspaceKeyResponse struct { - ID string `json:"_id"` - EncryptedKey string `json:"encryptedKey"` - Nonce string `json:"nonce"` - Sender struct { - ID string `json:"_id"` - Email string `json:"email"` - RefreshVersion int `json:"refreshVersion"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - V int `json:"__v"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - PublicKey string `json:"publicKey"` - } `json:"sender"` - Receiver string `json:"receiver"` - Workspace string `json:"workspace"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} - -// encrypted secret -type EncryptedSecret struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - SecretPath string `json:"secretPath"` -} - -// create secrets -type CreateSecretV3Request struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - SecretPath string `json:"secretPath"` -} - -// delete secret by name api -type DeleteSecretV3Request struct { - SecretName string `json:"secretName"` - WorkspaceId string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` -} - -// update secret by name api -type UpdateSecretByNameV3Request struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` -} - -// get secret by name api -type GetSingleSecretByNameV3Request struct { - SecretName string `json:"secretName"` - WorkspaceId string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` -} - -type GetSingleSecretByNameSecretResponse struct { - Secret EncryptedSecret `json:"secret"` -} - -type GetRawSecretsV3Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - SecretPath string `json:"secretPath"` -} - -type RawV3Secret struct { - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKey string `json:"secretKey"` - SecretValue string `json:"secretValue"` - SecretComment string `json:"secretComment"` -} - -type GetRawSecretsV3Response struct { - Secrets []RawV3Secret `json:"secrets"` -} - -type GetSingleRawSecretByNameSecretResponse struct { - Secret RawV3Secret `json:"secret"` -} - -// create secrets -type CreateRawSecretV3Request struct { - WorkspaceID string `json:"workspaceId"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKey string `json:"secretKey"` - SecretValue string `json:"secretValue"` - SecretComment string `json:"secretComment"` - SecretPath string `json:"secretPath"` -} - -type DeleteRawSecretV3Request struct { - SecretName string `json:"secretName"` - WorkspaceId string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` -} - -// update secret by name api -type UpdateRawSecretByNameV3Request struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` - SecretValue string `json:"secretValue"` -} - -type CreateProjectRequest struct { - ProjectName string `json:"projectName"` - Slug string `json:"slug"` - OrganizationSlug string `json:"organizationSlug"` -} - -type DeleteProjectRequest struct { - Slug string `json:"slug"` -} - -type GetProjectRequest struct { - Slug string `json:"slug"` -} - -type UpdateProjectRequest struct { - Slug string `json:"slug"` - ProjectName string `json:"name"` -} diff --git a/client/secret-operations.go b/client/secret-operations.go deleted file mode 100644 index de832cf..0000000 --- a/client/secret-operations.go +++ /dev/null @@ -1,78 +0,0 @@ -package infisicalclient - -import ( - "fmt" - "strings" -) - -func (client Client) GetPlainTextSecretsViaServiceToken(secretFolderPath string, envSlug string) ([]SingleEnvironmentVariable, *GetServiceTokenDetailsResponse, error) { - if client.Config.ServiceToken == "" { - return nil, nil, fmt.Errorf("service token must be defined to fetch secrets") - } - - serviceTokenParts := strings.SplitN(client.Config.ServiceToken, ".", 4) - if len(serviceTokenParts) < 4 { - return nil, nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") - } - - serviceTokenDetails, err := client.CallGetServiceTokenDetailsV2() - if err != nil { - return nil, nil, fmt.Errorf("unable to get service token details. [err=%v]", err) - } - - request := GetEncryptedSecretsV3Request{ - WorkspaceId: serviceTokenDetails.Workspace, - Environment: envSlug, - } - - if secretFolderPath != "" { - request.SecretPath = secretFolderPath - } - - encryptedSecrets, err := client.CallGetSecretsV3(request) - - if err != nil { - return nil, nil, err - } - - decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag) - if err != nil { - return nil, nil, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) - } - - plainTextWorkspaceKey, err := DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) - if err != nil { - return nil, nil, fmt.Errorf("unable to decrypt the required workspace key") - } - - plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) - if err != nil { - return nil, nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) - } - - return plainTextSecrets, &serviceTokenDetails, nil -} - -func (client Client) GetRawSecrets(secretFolderPath string, envSlug string, workspaceId string) ([]RawV3Secret, error) { - if client.Config.ClientId == "" || client.Config.ClientSecret == "" { - return nil, fmt.Errorf("client ID and client secret must be defined to fetch secrets with machine identity") - } - - request := GetRawSecretsV3Request{ - Environment: envSlug, - WorkspaceId: workspaceId, - } - - if secretFolderPath != "" { - request.SecretPath = secretFolderPath - } - - secrets, err := client.CallGetSecretsRawV3(request) - - if err != nil { - return nil, err - } - - return secrets.Secrets, nil - -} diff --git a/docs/data-sources/secrets.md b/docs/data-sources/secrets.md index 529d690..5fb3cfe 100644 --- a/docs/data-sources/secrets.md +++ b/docs/data-sources/secrets.md @@ -18,7 +18,6 @@ terraform { infisical = { # version = source = "infisical/infisical" - } } } @@ -73,4 +72,4 @@ Read-Only: - `comment` (String) The secret comment - `secret_type` (String) The secret type (shared or personal) -- `value` (String) The secret value +- `value` (String, Sensitive) The secret value diff --git a/docs/index.md b/docs/index.md index 27dd9ac..03a8704 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,6 @@ terraform { infisical = { # version = source = "infisical/infisical" - } } } diff --git a/docs/resources/project.md b/docs/resources/project.md index 2712a22..8d7584c 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -54,4 +54,5 @@ resource "infisical_project" "azure-project" { ### Read-Only +- `id` (String) The ID of the project - `last_updated` (String) diff --git a/docs/resources/project_identity.md b/docs/resources/project_identity.md new file mode 100644 index 0000000..2d810fd --- /dev/null +++ b/docs/resources/project_identity.md @@ -0,0 +1,89 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "infisical_project_identity Resource - terraform-provider-infisical" +subcategory: "" +description: |- + Create project identities & save to Infisical. Only Machine Identity authentication is supported for this data source +--- + +# infisical_project_identity (Resource) + +Create project identities & save to Infisical. Only Machine Identity authentication is supported for this data source + +## Example Usage + +```terraform +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_identity" "test-identity" { + project_id = infisical_project.example.id + identity_id = "" + roles = [ + { + role_slug = "admin" + } + ] +} +``` + + +## Schema + +### Required + +- `identity_id` (String) The id of the identity. +- `project_id` (String) The id of the project +- `roles` (Attributes List) The roles assigned to the project identity (see [below for nested schema](#nestedatt--roles)) + +### Read-Only + +- `identity` (Attributes) The identity details of the project identity (see [below for nested schema](#nestedatt--identity)) +- `membership_id` (String) The membership Id of the project identity + + +### Nested Schema for `roles` + +Required: + +- `role_slug` (String) The slug of the role + +Optional: + +- `custom_role_id` (String) The id of the custom role slug +- `is_temporary` (Boolean) Flag to indicate the assigned role is temporary or not. When is_temporary is true fields temporary_mode, temporary_range and temporary_access_start_time is required. +- `temporary_access_end_time` (String) ISO time for which temporary access will end. Computed based on temporary_range and temporary_access_start_time +- `temporary_access_start_time` (String) ISO time for which temporary access should begin. The current time is used by default. +- `temporary_mode` (String) Type of temporary access given. Types: relative. Default: relative +- `temporary_range` (String) TTL for the temporary time. Eg: 1m, 1h, 1d. Default: 1h + +Read-Only: + +- `id` (String) The ID of the project identity role. + + + +### Nested Schema for `identity` + +Read-Only: + +- `auth_method` (String) The auth method for the identity +- `id` (String) The ID of the identity +- `name` (String) The name of the identity diff --git a/docs/resources/project_identity_specific_privilege.md b/docs/resources/project_identity_specific_privilege.md new file mode 100644 index 0000000..bc3c317 --- /dev/null +++ b/docs/resources/project_identity_specific_privilege.md @@ -0,0 +1,100 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "infisical_project_identity_specific_privilege Resource - terraform-provider-infisical" +subcategory: "" +description: |- + Create additional privileges for identities & save to Infisical. Only Machine Identity authentication is supported for this data source. +--- + +# infisical_project_identity_specific_privilege (Resource) + +Create additional privileges for identities & save to Infisical. Only Machine Identity authentication is supported for this data source. + +## Example Usage + +```terraform +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_identity" "test-identity" { + project_id = infisical_project.example.id + identity_id = "" + roles = [ + { + role_slug = "admin" + } + ] +} + +resource "infisical_project_identity_specific_privilege" "test-privilege" { + project_slug = infisical_project.example.slug + identity_id = infisical_project_identity.test-identity.identity_id + permission = { + actions = ["read", "edit"] + subject = "secrets", + conditions = { + environment = "dev" + secret_path = "/dev" + } + } +} +``` + + +## Schema + +### Required + +- `identity_id` (String) The identity id to create identity specific privilege +- `permission` (Attributes) The permissions assigned to the project identity specific privilege (see [below for nested schema](#nestedatt--permission)) +- `project_slug` (String) The slug of the project to create identity specific privilege + +### Optional + +- `is_temporary` (Boolean) Flag to indicate the assigned specific privilege is temporary or not. When is_temporary is true fields temporary_mode, temporary_range and temporary_access_start_time is required. +- `slug` (String) The slug for the new privilege +- `temporary_access_end_time` (String) ISO time for which temporary access will end. Computed based on temporary_range and temporary_access_start_time +- `temporary_access_start_time` (String) ISO time for which temporary access should begin. The current time is used by default. +- `temporary_mode` (String) Type of temporary access given. Types: relative. Default: relative +- `temporary_range` (String) TTL for the temporary time. Eg: 1m, 1h, 1d. Default: 1h + +### Read-Only + +- `id` (String) The ID of the privilege + + +### Nested Schema for `permission` + +Required: + +- `actions` (List of String) Describe what action an entity can take. Enum: create,edit,delete,read +- `conditions` (Attributes) The conditions to scope permissions (see [below for nested schema](#nestedatt--permission--conditions)) +- `subject` (String) Describe what action an entity can take. Enum: role,member,groups,settings,integrations,webhooks,service-tokens,environments,tags,audit-logs,ip-allowlist,workspace,secrets,secret-rollback,secret-approval,secret-rotation,identity + + +### Nested Schema for `permission.conditions` + +Required: + +- `environment` (String) The environment slug this permission should allow. + +Optional: + +- `secret_path` (String) The secret path this permission should be scoped to diff --git a/docs/resources/project_role.md b/docs/resources/project_role.md new file mode 100644 index 0000000..bb7e7de --- /dev/null +++ b/docs/resources/project_role.md @@ -0,0 +1,90 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "infisical_project_role Resource - terraform-provider-infisical" +subcategory: "" +description: |- + Create custom project roles & save to Infisical. Only Machine Identity authentication is supported for this data source. +--- + +# infisical_project_role (Resource) + +Create custom project roles & save to Infisical. Only Machine Identity authentication is supported for this data source. + +## Example Usage + +```terraform +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_role" "biller" { + project_slug = infisical_project.example.slug + name = "Tester" + description = "A test role" + slug = "tester" + permissions = [ + { + action = "read" + subject = "secrets", + conditions = { + environment = "dev" + secret_path = "/dev" + } + }, + ] +} +``` + + +## Schema + +### Required + +- `name` (String) The name for the new role +- `permissions` (Attributes List) The permissions assigned to the project role (see [below for nested schema](#nestedatt--permissions)) +- `project_slug` (String) The slug of the project to create role +- `slug` (String) The slug for the new role + +### Optional + +- `description` (String) The description for the new role + +### Read-Only + +- `id` (String) The ID of the role + + +### Nested Schema for `permissions` + +Required: + +- `action` (String) Describe what action an entity can take. Enum: create,edit,delete,read +- `subject` (String) Describe what action an entity can take. Enum: role,member,groups,settings,integrations,webhooks,service-tokens,environments,tags,audit-logs,ip-allowlist,workspace,secrets,secret-rollback,secret-approval,secret-rotation,identity + +Optional: + +- `conditions` (Attributes) The conditions to scope permissions (see [below for nested schema](#nestedatt--permissions--conditions)) + + +### Nested Schema for `permissions.conditions` + +Optional: + +- `environment` (String) The environment slug this permission should allow. +- `secret_path` (String) The secret path this permission should be scoped to diff --git a/docs/resources/project_user.md b/docs/resources/project_user.md new file mode 100644 index 0000000..7ec4e4e --- /dev/null +++ b/docs/resources/project_user.md @@ -0,0 +1,90 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "infisical_project_user Resource - terraform-provider-infisical" +subcategory: "" +description: |- + Create project users & save to Infisical. Only Machine Identity authentication is supported for this data source +--- + +# infisical_project_user (Resource) + +Create project users & save to Infisical. Only Machine Identity authentication is supported for this data source + +## Example Usage + +```terraform +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_user" "test-user" { + project_id = infisical_project.example.id + username = "" + roles = [ + { + role_slug = "admin" + } + ] +} +``` + + +## Schema + +### Required + +- `project_id` (String) The id of the project +- `roles` (Attributes List) The roles assigned to the project user (see [below for nested schema](#nestedatt--roles)) +- `username` (String) The usename of the user. By default its the email + +### Read-Only + +- `membership_id` (String) The membershipId of the project user +- `user` (Attributes) The user details of the project user (see [below for nested schema](#nestedatt--user)) + + +### Nested Schema for `roles` + +Required: + +- `role_slug` (String) The slug of the role + +Optional: + +- `custom_role_id` (String) The id of the custom role slug +- `is_temporary` (Boolean) Flag to indicate the assigned role is temporary or not. When is_temporary is true fields temporary_mode, temporary_range and temporary_access_start_time is required. +- `temporary_access_end_time` (String) ISO time for which temporary access will end. Computed based on temporary_range and temporary_access_start_time +- `temporary_access_start_time` (String) ISO time for which temporary access should begin. The current time is used by default. +- `temporary_mode` (String) Type of temporary access given. Types: relative. Default: relative +- `temporary_range` (String) TTL for the temporary time. Eg: 1m, 1h, 1d. Default: 1h + +Read-Only: + +- `id` (String) The ID of the project user role. + + + +### Nested Schema for `user` + +Read-Only: + +- `email` (String) The email of the user +- `first_name` (String) The first name of the user +- `id` (String) The id of the user +- `last_name` (String) The last name of the user diff --git a/docs/resources/secret.md b/docs/resources/secret.md index 378ba5e..8a01fe3 100644 --- a/docs/resources/secret.md +++ b/docs/resources/secret.md @@ -61,7 +61,7 @@ resource "infisical_secret" "github_action_secret" { - `env_slug` (String) The environment slug of the secret to modify/create - `folder_path` (String) The path to the folder where the given secret resides - `name` (String) The name of the secret -- `value` (String) The value of the secret +- `value` (String, Sensitive) The value of the secret ### Optional diff --git a/examples/resources/infisical_project_identity/resource.tf b/examples/resources/infisical_project_identity/resource.tf new file mode 100644 index 0000000..0cd43ea --- /dev/null +++ b/examples/resources/infisical_project_identity/resource.tf @@ -0,0 +1,29 @@ +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_identity" "test-identity" { + project_id = infisical_project.example.id + identity_id = "" + roles = [ + { + role_slug = "admin" + } + ] +} diff --git a/examples/resources/infisical_project_identity_specific_privilege/resource.tf b/examples/resources/infisical_project_identity_specific_privilege/resource.tf new file mode 100644 index 0000000..a1d991d --- /dev/null +++ b/examples/resources/infisical_project_identity_specific_privilege/resource.tf @@ -0,0 +1,42 @@ +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_identity" "test-identity" { + project_id = infisical_project.example.id + identity_id = "" + roles = [ + { + role_slug = "admin" + } + ] +} + +resource "infisical_project_identity_specific_privilege" "test-privilege" { + project_slug = infisical_project.example.slug + identity_id = infisical_project_identity.test-identity.identity_id + permission = { + actions = ["read", "edit"] + subject = "secrets", + conditions = { + environment = "dev" + secret_path = "/dev" + } + } +} diff --git a/examples/resources/infisical_project_role/resource.tf b/examples/resources/infisical_project_role/resource.tf new file mode 100644 index 0000000..a1965c3 --- /dev/null +++ b/examples/resources/infisical_project_role/resource.tf @@ -0,0 +1,36 @@ +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_role" "biller" { + project_slug = infisical_project.example.slug + name = "Tester" + description = "A test role" + slug = "tester" + permissions = [ + { + action = "read" + subject = "secrets", + conditions = { + environment = "dev" + secret_path = "/dev" + } + }, + ] +} diff --git a/examples/resources/infisical_project_user/resource.tf b/examples/resources/infisical_project_user/resource.tf new file mode 100644 index 0000000..2e9cf62 --- /dev/null +++ b/examples/resources/infisical_project_user/resource.tf @@ -0,0 +1,29 @@ +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_project_user" "test-user" { + project_id = infisical_project.example.id + username = "" + roles = [ + { + role_slug = "admin" + } + ] +} diff --git a/infisical/provider/provider_test.go b/infisical/provider/provider_test.go deleted file mode 100644 index ef6599b..0000000 --- a/infisical/provider/provider_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-framework/providerserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" -) - -// testAccProtoV6ProviderFactories are used to instantiate a provider during -// acceptance testing. The factory function will be invoked for every Terraform -// CLI command executed to create a provider server to which the CLI can -// reattach. -var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ - "scaffolding": providerserver.NewProtocol6WithError(New("test")()), -} - -func testAccPreCheck(t *testing.T) { - // You can add code here to run prior to any test case execution, for example assertions - // about the appropriate environment variables being set are common to see in a pre-check - // function. -} diff --git a/infisical/util/util.go b/infisical/util/util.go deleted file mode 100644 index c7d8682..0000000 --- a/infisical/util/util.go +++ /dev/null @@ -1 +0,0 @@ -package util diff --git a/client/client.go b/internal/client/client.go similarity index 100% rename from client/client.go rename to internal/client/client.go diff --git a/client/client_test.go b/internal/client/client_test.go similarity index 100% rename from client/client_test.go rename to internal/client/client_test.go diff --git a/internal/client/constants.go b/internal/client/constants.go new file mode 100644 index 0000000..01a74f3 --- /dev/null +++ b/internal/client/constants.go @@ -0,0 +1,3 @@ +package infisicalclient + +const USER_AGENT = "terraform" diff --git a/internal/client/helpers.go b/internal/client/helpers.go new file mode 100644 index 0000000..c9aa28d --- /dev/null +++ b/internal/client/helpers.go @@ -0,0 +1,53 @@ +package infisicalclient + +import ( + "encoding/base64" + "errors" + "fmt" + "strings" +) + +type DecodedSymmetricEncryptionDetails = struct { + Cipher []byte + IV []byte + Tag []byte + Key []byte +} + +func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) { + cipherx, err := base64.StdEncoding.DecodeString(cipher) + if err != nil { + return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err) + } + + keyx, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err) + } + + IVx, err := base64.StdEncoding.DecodeString(IV) + if err != nil { + return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err) + } + + tagx, err := base64.StdEncoding.DecodeString(tag) + if err != nil { + return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err) + } + + return DecodedSymmetricEncryptionDetails{ + Key: keyx, + Cipher: cipherx, + IV: IVx, + Tag: tagx, + }, nil +} + +func GetSymmetricKeyFromServiceToken(serviceToken string) (privateKey string, err error) { + serviceTokenParts := strings.SplitN(serviceToken, ".", 4) + if len(serviceTokenParts) < 4 { + return "", errors.New("invalid service token entered. Please double check your service token and try again") + } + + return serviceTokenParts[3], nil +} diff --git a/internal/client/login.go b/internal/client/login.go new file mode 100644 index 0000000..885b0b9 --- /dev/null +++ b/internal/client/login.go @@ -0,0 +1,45 @@ +package infisicalclient + +import "fmt" + +func (client Client) UniversalMachineIdentityAuth() (string, error) { + if client.Config.ClientId == "" || client.Config.ClientSecret == "" { + return "", fmt.Errorf("you must set the client secret and client ID for the client before making calls") + } + + var loginResponse UniversalMachineIdentityAuthResponse + + res, err := client.Config.HttpClient.R().SetResult(&loginResponse).SetHeader("User-Agent", USER_AGENT).SetBody(map[string]string{ + "clientId": client.Config.ClientId, + "clientSecret": client.Config.ClientSecret, + }).Post("api/v1/auth/universal-auth/login") + + if err != nil { + return "", fmt.Errorf("UniversalMachineIdentityAuth: Unable to complete api request [err=%s]", err) + } + + if res.IsError() { + return "", fmt.Errorf("UniversalMachineIdentityAuth: Unsuccessful response: [response=%s]", res) + } + + return loginResponse.AccessToken, nil +} + +func (client Client) GetServiceTokenDetailsV2() (GetServiceTokenDetailsResponse, error) { + var tokenDetailsResponse GetServiceTokenDetailsResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&tokenDetailsResponse). + SetHeader("User-Agent", USER_AGENT). + Get("api/v2/service-token") + + if err != nil { + return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response) + } + + return tokenDetailsResponse, nil +} diff --git a/internal/client/model.go b/internal/client/model.go new file mode 100644 index 0000000..49fbf73 --- /dev/null +++ b/internal/client/model.go @@ -0,0 +1,630 @@ +package infisicalclient + +import ( + "time" +) + +type GetEncryptedSecretsV3Request struct { + Environment string `json:"environment"` + WorkspaceId string `json:"workspaceId"` + SecretPath string `json:"secretPath"` +} + +type EncryptedSecretV3 struct { + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + Algorithm string `json:"algorithm"` + KeyEncoding string `json:"keyEncoding"` + Folder string `json:"folder"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type Project struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + AutoCapitalization bool `json:"autoCapitalization"` + OrgID string `json:"orgId"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Version int `json:"version"` + + UpgradeStatus string `json:"upgradeStatus"` // can be null. if its null it will be converted to an empty string. +} + +type ProjectUser struct { + ID string `json:"id"` + UserID string `json:"userId"` + User struct { + Email string `json:"email"` + ID string `json:"id"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + PublicKey string `json:"publicKey"` + } `json:"user"` + Roles []ProjectMemberRole +} + +type ProjectIdentity struct { + ID string `json:"id"` + IdentityID string `json:"identityId"` + Roles []ProjectMemberRole + Identity struct { + Name string `json:"name"` + Id string `json:"id"` + AuthMethod string `json:"authMethod"` + } `json:"identity"` +} + +type ProjectMemberRole struct { + ID string `json:"id"` + Role string `json:"role"` + CustomRoleSlug string `json:"customRoleSlug"` + ProjectMembershipId string `json:"projectMembershipId"` + CustomRoleId string `json:"customRoleId"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` + TemporaryAccessEndTime time.Time `json:"temporaryAccessEndTime"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type ProjectIdentitySpecificPrivilege struct { + ID string `json:"id"` + Slug string `json:"slug"` + ProjectMembershipId string `json:"projectMembershipId"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` + TemporaryAccessEndTime time.Time `json:"temporaryAccessEndTime"` + // because permission can have multiple structure. + Permissions []map[string]any + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type ProjectRole struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + // because permission can have multiple structure. + Permissions []map[string]any +} + +type ProjectWithEnvironments struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + AutoCapitalization bool `json:"autoCapitalization"` + OrgID string `json:"orgId"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + Version int64 `json:"version"` + UpgradeStatus string `json:"upgradeStatus"` + Environments []ProjectEnvironment `json:"environments"` +} + +type ProjectMemberships struct { + ID string `json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + UserID string `json:"userId"` + ProjectID string `json:"projectId"` +} + +type ProjectEnvironment struct { + Name string `json:"name"` + Slug string `json:"slug"` + ID string `json:"id"` +} + +type CreateProjectResponse struct { + Project Project `json:"project"` +} + +type InviteUsersToProjectResponse struct { + Members []ProjectMemberships `json:"memberships"` +} + +type DeleteProjectResponse struct { + Project Project `json:"workspace"` +} + +type UpdateProjectResponse Project + +type GetEncryptedSecretsV3Response struct { + Secrets []EncryptedSecretV3 `json:"secrets"` +} + +type GetServiceTokenDetailsResponse struct { + ID string `json:"_id"` + Name string `json:"name"` + Workspace string `json:"workspace"` + Environment string `json:"environment"` + ExpiresAt time.Time `json:"expiresAt"` + EncryptedKey string `json:"encryptedKey"` + Iv string `json:"iv"` + Tag string `json:"tag"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + V int `json:"__v"` +} + +type UniversalMachineIdentityAuthResponse struct { + AccessToken string `json:"accessToken"` + ExpiresIn int `json:"expiresIn"` + AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` + TokenType string `json:"tokenType"` +} + +type SingleEnvironmentVariable struct { + Key string `json:"key"` + Value string `json:"value"` + Type string `json:"type"` + ID string `json:"_id"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` + Comment string `json:"comment"` +} + +// Workspace key request. +type GetEncryptedWorkspaceKeyRequest struct { + WorkspaceId string `json:"workspaceId"` +} + +// Workspace key response. +type GetEncryptedWorkspaceKeyResponse struct { + ID string `json:"_id"` + EncryptedKey string `json:"encryptedKey"` + Nonce string `json:"nonce"` + Sender struct { + ID string `json:"_id"` + Email string `json:"email"` + RefreshVersion int `json:"refreshVersion"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + V int `json:"__v"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + PublicKey string `json:"publicKey"` + } `json:"sender"` + Receiver string `json:"receiver"` + Workspace string `json:"workspace"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// encrypted secret. +type EncryptedSecret struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + SecretPath string `json:"secretPath"` +} + +// create secrets. +type CreateSecretV3Request struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + SecretPath string `json:"secretPath"` +} + +// delete secret by name api. +type DeleteSecretV3Request struct { + SecretName string `json:"secretName"` + WorkspaceId string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` +} + +// update secret by name api. +type UpdateSecretByNameV3Request struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` +} + +// get secret by name api. +type GetSingleSecretByNameV3Request struct { + SecretName string `json:"secretName"` + WorkspaceId string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` +} + +type GetSingleSecretByNameSecretResponse struct { + Secret EncryptedSecret `json:"secret"` +} + +type GetRawSecretsV3Request struct { + Environment string `json:"environment"` + WorkspaceId string `json:"workspaceId"` + SecretPath string `json:"secretPath"` +} + +type RawV3Secret struct { + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKey string `json:"secretKey"` + SecretValue string `json:"secretValue"` + SecretComment string `json:"secretComment"` +} + +type GetRawSecretsV3Response struct { + Secrets []RawV3Secret `json:"secrets"` +} + +type GetSingleRawSecretByNameSecretResponse struct { + Secret RawV3Secret `json:"secret"` +} + +// create secrets. +type CreateRawSecretV3Request struct { + WorkspaceID string `json:"workspaceId"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKey string `json:"secretKey"` + SecretValue string `json:"secretValue"` + SecretComment string `json:"secretComment"` + SecretPath string `json:"secretPath"` +} + +type DeleteRawSecretV3Request struct { + SecretName string `json:"secretName"` + WorkspaceId string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` +} + +// update secret by name api. +type UpdateRawSecretByNameV3Request struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` + SecretValue string `json:"secretValue"` +} + +type CreateProjectRequest struct { + ProjectName string `json:"projectName"` + Slug string `json:"slug"` + OrganizationSlug string `json:"organizationSlug"` +} + +type DeleteProjectRequest struct { + Slug string `json:"slug"` +} + +type GetProjectRequest struct { + Slug string `json:"slug"` +} + +type UpdateProjectRequest struct { + Slug string `json:"slug"` + ProjectName string `json:"name"` +} + +type InviteUsersToProjectRequest struct { + ProjectID string `json:"projectId"` + Usernames []string `json:"usernames"` +} + +type CreateProjectUserRequest struct { + ProjectID string `json:"projectId"` + Username []string `json:"usernames"` +} + +type CreateProjectUserResponse struct { + Memberships []CreateProjectUserResponseMembers `json:"memberships"` +} + +type CreateProjectUserResponseMembers struct { + ID string `json:"id"` + UserId string `json:"userId"` +} + +type GetProjectUserByUserNameRequest struct { + ProjectID string `json:"projectId"` + Username string `json:"username"` +} + +type GetProjectUserByUserNameResponse struct { + Membership ProjectUser `json:"membership"` +} + +type UpdateProjectUserRequest struct { + ProjectID string `json:"projectId"` + MembershipID string `json:"membershipId"` + Roles []UpdateProjectUserRequestRoles `json:"roles"` +} + +type UpdateProjectUserRequestRoles struct { + Role string `json:"role"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` +} + +type UpdateProjectUserResponse struct { + Roles []struct { + ID string `json:"id"` + Role string `json:"role"` + ProjectMembershipId string `json:"projectMembershipId"` + CustomRoleId string `json:"customRoleId"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` + TemporaryAccessEndTime time.Time `json:"temporaryAccessEndTime"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"roles"` +} + +type DeleteProjectUserRequest struct { + ProjectID string `json:"projectId"` + Username []string `json:"usernames"` +} + +type DeleteProjectUserResponse struct { + Memberships []DeleteProjectUserResponseMembers `json:"memberships"` +} + +type DeleteProjectUserResponseMembers struct { + ID string `json:"id"` + UserId string `json:"userId"` +} + +// identity. +type CreateProjectIdentityRequest struct { + ProjectID string `json:"projectId"` + IdentityID string `json:"identityId"` + Roles []CreateProjectIdentityRequestRoles `json:"roles"` +} + +type CreateProjectIdentityRequestRoles struct { + Role string `json:"role"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` +} + +type CreateProjectIdentityResponse struct { + Membership CreateProjectIdentityResponseMembers `json:"identityMembership"` +} + +type CreateProjectIdentityResponseMembers struct { + ID string `json:"id"` + IdentityId string `json:"identityId"` +} + +type GetProjectIdentityByIDRequest struct { + ProjectID string `json:"projectId"` + IdentityID string `json:"identityId"` +} + +type GetProjectIdentityByIDResponse struct { + Membership ProjectIdentity `json:"identityMembership"` +} + +type UpdateProjectIdentityRequest struct { + ProjectID string `json:"projectId"` + IdentityID string `json:"identityId"` + Roles []UpdateProjectIdentityRequestRoles `json:"roles"` +} + +type UpdateProjectIdentityRequestRoles struct { + Role string `json:"role"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` +} + +type UpdateProjectIdentityResponse struct { + Roles []struct { + ID string `json:"id"` + Role string `json:"role"` + ProjectMembershipId string `json:"projectMembershipId"` + CustomRoleId string `json:"customRoleId"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` + TemporaryAccessEndTime time.Time `json:"temporaryAccessEndTime"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"roles"` +} + +type DeleteProjectIdentityRequest struct { + ProjectID string `json:"projectId"` + IdentityID string `json:"identityId"` +} + +type DeleteProjectIdentityResponse struct { + Membership DeleteProjectIdentityResponseIdentities `json:"identityMembership"` +} + +type DeleteProjectIdentityResponseIdentities struct { + ID string `json:"id"` + IdentityID string `json:"identityId"` +} + +type CreateProjectRoleRequest struct { + ProjectSlug string `json:"projectSlug"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + Permissions []ProjectRolePermissionRequest `json:"permissions"` +} + +type CreateProjectRoleResponse struct { + Role ProjectRole `json:"role"` +} + +type UpdateProjectRoleRequest struct { + ProjectSlug string `json:"projectSlug"` + RoleId string `json:"roleId"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + Permissions []ProjectRolePermissionRequest `json:"permissions"` +} + +type UpdateProjectRoleResponse struct { + Role ProjectRole `json:"role"` +} + +type DeleteProjectRoleRequest struct { + ProjectSlug string `json:"projectSlug"` + RoleId string `json:"roleId"` +} + +type DeleteProjectRoleResponse struct { + Role ProjectRole `json:"role"` +} + +type GetProjectRoleBySlugRequest struct { + ProjectSlug string `json:"projectSlug"` + RoleSlug string `json:"roleSlug"` +} + +type GetProjectRoleBySlugResponse struct { + Role ProjectRole `json:"role"` +} + +type ProjectRolePermissionRequest struct { + Action string `json:"action"` + Subject string `json:"subject"` + Conditions map[string]any `json:"conditions,omitempty"` +} + +type ProjectSpecificPrivilegePermissionRequest struct { + Actions []string `json:"actions"` + Subject string `json:"subject"` + Conditions map[string]any `json:"conditions,omitempty"` +} + +type CreatePermanentProjectIdentitySpecificPrivilegeRequest struct { + ProjectSlug string `json:"projectSlug"` + IdentityId string `json:"identityId"` + Slug string `json:"slug,omitempty"` + Permissions ProjectSpecificPrivilegePermissionRequest `json:"privilegePermission"` +} + +type CreateTemporaryProjectIdentitySpecificPrivilegeRequest struct { + ProjectSlug string `json:"projectSlug"` + IdentityId string `json:"identityId"` + Slug string `json:"slug,omitempty"` + Permissions ProjectSpecificPrivilegePermissionRequest `json:"privilegePermission"` + TemporaryMode string `json:"temporaryMode"` + TemporaryRange string `json:"temporaryRange"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime"` +} + +type CreateProjectIdentitySpecificPrivilegeResponse struct { + Privilege ProjectIdentitySpecificPrivilege `json:"privilege"` +} + +type UpdateProjectIdentitySpecificPrivilegeRequest struct { + ProjectSlug string `json:"projectSlug"` + IdentityId string `json:"identityId"` + PrivilegeSlug string `json:"privilegeSlug,omitempty"` + Details UpdateProjectIdentitySpecificPrivilegeDataRequest `json:"privilegeDetails"` +} + +type UpdateProjectIdentitySpecificPrivilegeDataRequest struct { + Slug string `json:"slug,omitempty"` + Permissions ProjectSpecificPrivilegePermissionRequest `json:"privilegePermission"` + IsTemporary bool `json:"isTemporary"` + TemporaryMode string `json:"temporaryMode,omitempty"` + TemporaryRange string `json:"temporaryRange,omitempty"` + TemporaryAccessStartTime time.Time `json:"temporaryAccessStartTime,omitempty"` +} + +type UpdateProjectIdentitySpecificPrivilegeResponse struct { + Privilege ProjectIdentitySpecificPrivilege `json:"privilege"` +} + +type DeleteProjectIdentitySpecificPrivilegeRequest struct { + ProjectSlug string `json:"projectSlug"` + IdentityId string `json:"identityId"` + PrivilegeSlug string `json:"privilegeSlug,omitempty"` +} + +type DeleteProjectIdentitySpecificPrivilegeResponse struct { + Privilege ProjectIdentitySpecificPrivilege `json:"privilege"` +} + +type GetProjectIdentitySpecificPrivilegeRequest struct { + ProjectSlug string `json:"projectSlug"` + IdentityId string `json:"identityId"` + PrivilegeSlug string `json:"privilegeSlug,omitempty"` +} + +type GetProjectIdentitySpecificPrivilegeResponse struct { + Privilege ProjectIdentitySpecificPrivilege `json:"privilege"` +} diff --git a/internal/client/project.go b/internal/client/project.go new file mode 100644 index 0000000..17a7dbc --- /dev/null +++ b/internal/client/project.go @@ -0,0 +1,89 @@ +package infisicalclient + +import "fmt" + +func (client Client) CreateProject(request CreateProjectRequest) (CreateProjectResponse, error) { + + if request.Slug == "" { + request = CreateProjectRequest{ + ProjectName: request.ProjectName, + OrganizationSlug: request.OrganizationSlug, + } + } + + var projectResponse CreateProjectResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&projectResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post("api/v2/workspace") + + if err != nil { + return CreateProjectResponse{}, fmt.Errorf("CallCreateProject: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return CreateProjectResponse{}, fmt.Errorf("CallCreateProject: Unsuccessful response. [response=%s]", response) + } + + return projectResponse, nil +} + +func (client Client) DeleteProject(request DeleteProjectRequest) error { + var projectResponse DeleteProjectResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&projectResponse). + SetHeader("User-Agent", USER_AGENT). + Delete(fmt.Sprintf("api/v2/workspace/%s", request.Slug)) + + if err != nil { + return fmt.Errorf("CallDeleteProject: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallDeleteProject: Unsuccessful response. [response=%s]", response) + } + + return nil +} + +func (client Client) GetProject(request GetProjectRequest) (ProjectWithEnvironments, error) { + var projectResponse ProjectWithEnvironments + response, err := client.Config.HttpClient. + R(). + SetResult(&projectResponse). + SetHeader("User-Agent", USER_AGENT). + Get(fmt.Sprintf("api/v2/workspace/%s", request.Slug)) + + if err != nil { + return ProjectWithEnvironments{}, fmt.Errorf("CallGetProject: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return ProjectWithEnvironments{}, fmt.Errorf("CallGetProject: Unsuccessful response. [response=%s]", response) + } + + return projectResponse, nil +} + +func (client Client) UpdateProject(request UpdateProjectRequest) (UpdateProjectResponse, error) { + var projectResponse UpdateProjectResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&projectResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("api/v2/workspace/%s", request.Slug)) + + if err != nil { + return UpdateProjectResponse{}, fmt.Errorf("CallUpdateProject: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return UpdateProjectResponse{}, fmt.Errorf("CallUpdateProject: Unsuccessful response. [response=%s]", response) + } + + return projectResponse, nil +} diff --git a/internal/client/project_identity.go b/internal/client/project_identity.go new file mode 100644 index 0000000..72da5a1 --- /dev/null +++ b/internal/client/project_identity.go @@ -0,0 +1,83 @@ +package infisicalclient + +import "fmt" + +func (client Client) CreateProjectIdentity(request CreateProjectIdentityRequest) (CreateProjectIdentityResponse, error) { + var responeData CreateProjectIdentityResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responeData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("api/v2/workspace/%s/identity-memberships/%s", request.ProjectID, request.IdentityID)) + + if err != nil { + return CreateProjectIdentityResponse{}, fmt.Errorf("CallCreateProjectIdentity: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return CreateProjectIdentityResponse{}, fmt.Errorf("CallCreateProjectIdentity: Unsuccessful response. [response=%s]", response) + } + + return responeData, nil +} + +func (client Client) DeleteProjectIdentity(request DeleteProjectIdentityRequest) (DeleteProjectIdentityResponse, error) { + var responseData DeleteProjectIdentityResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Delete(fmt.Sprintf("/api/v2/workspace/%s/identity-memberships/%s", request.ProjectID, request.IdentityID)) + + if err != nil { + return DeleteProjectIdentityResponse{}, fmt.Errorf("CallDeleteProjectIdentity: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return DeleteProjectIdentityResponse{}, fmt.Errorf("CallDeleteProjectIdentity: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} + +func (client Client) UpdateProjectIdentity(request UpdateProjectIdentityRequest) (UpdateProjectIdentityResponse, error) { + var responseData UpdateProjectIdentityResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("api/v2/workspace/%s/identity-memberships/%s", request.ProjectID, request.IdentityID)) + + if err != nil { + return UpdateProjectIdentityResponse{}, fmt.Errorf("CallUpdateProjectIdentity: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return UpdateProjectIdentityResponse{}, fmt.Errorf("CallUpdateProjectIdentity: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} + +func (client Client) GetProjectIdentityByID(request GetProjectIdentityByIDRequest) (GetProjectIdentityByIDResponse, error) { + var responseData GetProjectIdentityByIDResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Get(fmt.Sprintf("api/v2/workspace/%s/identity-memberships/%s", request.ProjectID, request.IdentityID)) + + if err != nil { + return GetProjectIdentityByIDResponse{}, fmt.Errorf("GetProjectIdentityByIDResponse: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetProjectIdentityByIDResponse{}, fmt.Errorf("GetProjectIdentityByIDResponse: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} diff --git a/internal/client/project_identity_specific_privilege.go b/internal/client/project_identity_specific_privilege.go new file mode 100644 index 0000000..222c58c --- /dev/null +++ b/internal/client/project_identity_specific_privilege.go @@ -0,0 +1,103 @@ +package infisicalclient + +import "fmt" + +func (client Client) CreatePermanentProjectIdentitySpecificPrivilege(request CreatePermanentProjectIdentitySpecificPrivilegeRequest) (CreateProjectIdentitySpecificPrivilegeResponse, error) { + var responeData CreateProjectIdentitySpecificPrivilegeResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responeData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post("/api/v1/additional-privilege/identity/permanent") + + if err != nil { + return CreateProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("CreatePermanentProjectIdentitySpecificPrivilege: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return CreateProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("CreatePermanentProjectIdentitySpecificPrivilege: Unsuccessful response. [response=%s]", response) + } + + return responeData, nil +} + +func (client Client) CreateTemporaryProjectIdentitySpecificPrivilege(request CreateTemporaryProjectIdentitySpecificPrivilegeRequest) (CreateProjectIdentitySpecificPrivilegeResponse, error) { + var responeData CreateProjectIdentitySpecificPrivilegeResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responeData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post("/api/v1/additional-privilege/identity/temporary") + + if err != nil { + return CreateProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("CreateTemporaryProjectIdentitySpecificPrivilege: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return CreateProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("CreateTemporaryProjectIdentitySpecificPrivilege: Unsuccessful response. [response=%s]", response) + } + + return responeData, nil +} + +func (client Client) DeleteProjectIdentitySpecificPrivilege(request DeleteProjectIdentitySpecificPrivilegeRequest) (DeleteProjectIdentitySpecificPrivilegeResponse, error) { + var responseData DeleteProjectIdentitySpecificPrivilegeResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Delete("/api/v1/additional-privilege/identity") + + if err != nil { + return DeleteProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("DeleteProjectIdentitySpecificPrivilege: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return DeleteProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("DeleteProjectIdentitySpecificPrivilege: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} + +func (client Client) UpdateProjectIdentitySpecificPrivilege(request UpdateProjectIdentitySpecificPrivilegeRequest) (UpdateProjectIdentitySpecificPrivilegeResponse, error) { + var responseData UpdateProjectIdentitySpecificPrivilegeResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch("/api/v1/additional-privilege/identity") + + if err != nil { + return UpdateProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("UpdateProjectIdentitySpecificPrivilege: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return UpdateProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("UpdateProjectIdentitySpecificPrivilege: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} + +func (client Client) GetProjectIdentitySpecificPrivilegeBySlug(request GetProjectIdentitySpecificPrivilegeRequest) (GetProjectIdentitySpecificPrivilegeResponse, error) { + var responseData GetProjectIdentitySpecificPrivilegeResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Get(fmt.Sprintf("/api/v1/additional-privilege/identity/%s?projectSlug=%s&identityId=%s", request.PrivilegeSlug, request.ProjectSlug, request.IdentityId)) + + if err != nil { + return GetProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("GetProjectIdentitySpecificPrivilegeBySlug: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetProjectIdentitySpecificPrivilegeResponse{}, fmt.Errorf("GetProjectIdentitySpecificPrivilegeBySlug: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} diff --git a/internal/client/project_role.go b/internal/client/project_role.go new file mode 100644 index 0000000..05b7f19 --- /dev/null +++ b/internal/client/project_role.go @@ -0,0 +1,83 @@ +package infisicalclient + +import "fmt" + +func (client Client) CreateProjectRole(request CreateProjectRoleRequest) (CreateProjectRoleResponse, error) { + var responeData CreateProjectRoleResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responeData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("api/v1/workspace/%s/roles", request.ProjectSlug)) + + if err != nil { + return CreateProjectRoleResponse{}, fmt.Errorf("CreateProjectRole: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return CreateProjectRoleResponse{}, fmt.Errorf("CreateProjectRole: Unsuccessful response. [response=%s]", response) + } + + return responeData, nil +} + +func (client Client) DeleteProjectRole(request DeleteProjectRoleRequest) (DeleteProjectRoleResponse, error) { + var responseData DeleteProjectRoleResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Delete(fmt.Sprintf("/api/v1/workspace/%s/roles/%s", request.ProjectSlug, request.RoleId)) + + if err != nil { + return DeleteProjectRoleResponse{}, fmt.Errorf("DeleteProjectRole: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return DeleteProjectRoleResponse{}, fmt.Errorf("DeleteProjectRole: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} + +func (client Client) UpdateProjectRole(request UpdateProjectRoleRequest) (UpdateProjectRoleResponse, error) { + var responseData UpdateProjectRoleResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("api/v1/workspace/%s/roles/%s", request.ProjectSlug, request.RoleId)) + + if err != nil { + return UpdateProjectRoleResponse{}, fmt.Errorf("UpdateProjectRole: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return UpdateProjectRoleResponse{}, fmt.Errorf("UpdateProjectRole: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} + +func (client Client) GetProjectRoleBySlug(request GetProjectRoleBySlugRequest) (GetProjectRoleBySlugResponse, error) { + var responseData GetProjectRoleBySlugResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&responseData). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Get(fmt.Sprintf("api/v1/workspace/%s/roles/slug/%s", request.ProjectSlug, request.RoleSlug)) + + if err != nil { + return GetProjectRoleBySlugResponse{}, fmt.Errorf("GetProjectRoleBySlug: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetProjectRoleBySlugResponse{}, fmt.Errorf("GetProjectRoleBySlug: Unsuccessful response. [response=%s]", response) + } + + return responseData, nil +} diff --git a/internal/client/project_user.go b/internal/client/project_user.go new file mode 100644 index 0000000..931653b --- /dev/null +++ b/internal/client/project_user.go @@ -0,0 +1,83 @@ +package infisicalclient + +import "fmt" + +func (client Client) InviteUsersToProject(request InviteUsersToProjectRequest) ([]ProjectMemberships, error) { + var inviteUsersToProjectResponse InviteUsersToProjectResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&inviteUsersToProjectResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("api/v2/workspace/%s/memberships", request.ProjectID)) + + if err != nil { + return nil, fmt.Errorf("CallInviteUsersToProject: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return nil, fmt.Errorf("InviteUsersToProjectRequest: Unsuccessful response. [response=%s]", response) + } + + return inviteUsersToProjectResponse.Members, nil +} + +func (client Client) DeleteProjectUser(request DeleteProjectUserRequest) (DeleteProjectUserResponse, error) { + var projectUserResponse DeleteProjectUserResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&projectUserResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Delete(fmt.Sprintf("api/v2/workspace/%s/memberships", request.ProjectID)) + + if err != nil { + return DeleteProjectUserResponse{}, fmt.Errorf("CallDeleteProjectUser: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return DeleteProjectUserResponse{}, fmt.Errorf("CallDeleteProjectUser: Unsuccessful response. [response=%s]", response) + } + + return projectUserResponse, nil +} + +func (client Client) UpdateProjectUser(request UpdateProjectUserRequest) (UpdateProjectUserResponse, error) { + var projectUserResponse UpdateProjectUserResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&projectUserResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("api/v1/workspace/%s/memberships/%s", request.ProjectID, request.MembershipID)) + + if err != nil { + return UpdateProjectUserResponse{}, fmt.Errorf("UpdateProjectUserResponse: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return UpdateProjectUserResponse{}, fmt.Errorf("UpdateProjectUserResponse: Unsuccessful response. [response=%s]", response) + } + + return projectUserResponse, nil +} + +func (client Client) GetProjectUserByUsername(request GetProjectUserByUserNameRequest) (GetProjectUserByUserNameResponse, error) { + var projectUserResponse GetProjectUserByUserNameResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&projectUserResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("api/v1/workspace/%s/memberships/details", request.ProjectID)) + + if err != nil { + return GetProjectUserByUserNameResponse{}, fmt.Errorf("CallCreateProject: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetProjectUserByUserNameResponse{}, fmt.Errorf("CallCreateProject: Unsuccessful response. [response=%s]", response) + } + + return projectUserResponse, nil +} diff --git a/client/api.go b/internal/client/secrets.go similarity index 55% rename from client/api.go rename to internal/client/secrets.go index 09da811..5b1182e 100644 --- a/client/api.go +++ b/internal/client/secrets.go @@ -1,54 +1,13 @@ package infisicalclient import ( + "encoding/base64" "fmt" + "strings" + "terraform-provider-infisical/internal/crypto" ) -const USER_AGENT = "terraform" - -func (client Client) UniversalMachineIdentityAuth() (string, error) { - if client.Config.ClientId == "" || client.Config.ClientSecret == "" { - return "", fmt.Errorf("you must set the client secret and client ID for the client before making calls") - } - - var loginResponse UniversalMachineIdentityAuthResponse - - res, err := client.Config.HttpClient.R().SetResult(&loginResponse).SetHeader("User-Agent", USER_AGENT).SetBody(map[string]string{ - "clientId": client.Config.ClientId, - "clientSecret": client.Config.ClientSecret, - }).Post("api/v1/auth/universal-auth/login") - - if err != nil { - return "", fmt.Errorf("UniversalMachineIdentityAuth: Unable to complete api request [err=%s]", err) - } - - if res.IsError() { - return "", fmt.Errorf("UniversalMachineIdentityAuth: Unsuccessful response: [response=%s]", res) - } - - return loginResponse.AccessToken, nil -} - -func (client Client) CallGetServiceTokenDetailsV2() (GetServiceTokenDetailsResponse, error) { - var tokenDetailsResponse GetServiceTokenDetailsResponse - response, err := client.Config.HttpClient. - R(). - SetResult(&tokenDetailsResponse). - SetHeader("User-Agent", USER_AGENT). - Get("api/v2/service-token") - - if err != nil { - return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response) - } - - return tokenDetailsResponse, nil -} - -func (client Client) CallGetSecretsV3(request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) { +func (client Client) GetSecretsV3(request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) { var secretsResponse GetEncryptedSecretsV3Response httpRequest := client.Config.HttpClient. @@ -75,7 +34,7 @@ func (client Client) CallGetSecretsV3(request GetEncryptedSecretsV3Request) (Get return secretsResponse, nil } -func (client Client) CallCreateSecretsV3(request CreateSecretV3Request) error { +func (client Client) CreateSecretsV3(request CreateSecretV3Request) error { var secretsResponse EncryptedSecretV3 response, err := client.Config.HttpClient. R(). @@ -95,7 +54,7 @@ func (client Client) CallCreateSecretsV3(request CreateSecretV3Request) error { return nil } -func (client Client) CallDeleteSecretsV3(request DeleteSecretV3Request) error { +func (client Client) DeleteSecretsV3(request DeleteSecretV3Request) error { var secretsResponse GetEncryptedSecretsV3Response response, err := client.Config.HttpClient. R(). @@ -115,7 +74,7 @@ func (client Client) CallDeleteSecretsV3(request DeleteSecretV3Request) error { return nil } -func (client Client) CallUpdateSecretsV3(request UpdateSecretByNameV3Request) error { +func (client Client) UpdateSecretsV3(request UpdateSecretByNameV3Request) error { var secretsResponse GetEncryptedSecretsV3Response response, err := client.Config.HttpClient. @@ -136,7 +95,7 @@ func (client Client) CallUpdateSecretsV3(request UpdateSecretByNameV3Request) er return nil } -func (client Client) CallGetSingleSecretByNameV3(request GetSingleSecretByNameV3Request) (GetSingleSecretByNameSecretResponse, error) { +func (client Client) GetSingleSecretByNameV3(request GetSingleSecretByNameV3Request) (GetSingleSecretByNameSecretResponse, error) { var secretsResponse GetSingleSecretByNameSecretResponse response, err := client.Config.HttpClient. R(). @@ -159,7 +118,7 @@ func (client Client) CallGetSingleSecretByNameV3(request GetSingleSecretByNameV3 return secretsResponse, nil } -func (client Client) CallGetSecretsRawV3(request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) { +func (client Client) GetSecretsRawV3(request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) { var secretsResponse GetRawSecretsV3Response httpRequest := client.Config.HttpClient. @@ -186,7 +145,7 @@ func (client Client) CallGetSecretsRawV3(request GetRawSecretsV3Request) (GetRaw return secretsResponse, nil } -func (client Client) CallCreateRawSecretsV3(request CreateRawSecretV3Request) error { +func (client Client) CreateRawSecretsV3(request CreateRawSecretV3Request) error { var secretsResponse EncryptedSecretV3 response, err := client.Config.HttpClient. R(). @@ -206,7 +165,7 @@ func (client Client) CallCreateRawSecretsV3(request CreateRawSecretV3Request) er return nil } -func (client Client) CallDeleteRawSecretV3(request DeleteRawSecretV3Request) error { +func (client Client) DeleteRawSecretV3(request DeleteRawSecretV3Request) error { var secretsResponse GetRawSecretsV3Response response, err := client.Config.HttpClient. R(). @@ -226,7 +185,7 @@ func (client Client) CallDeleteRawSecretV3(request DeleteRawSecretV3Request) err return nil } -func (client Client) CallUpdateRawSecretV3(request UpdateRawSecretByNameV3Request) error { +func (client Client) UpdateRawSecretV3(request UpdateRawSecretByNameV3Request) error { var secretsResponse GetRawSecretsV3Response response, err := client.Config.HttpClient. R(). @@ -246,7 +205,7 @@ func (client Client) CallUpdateRawSecretV3(request UpdateRawSecretByNameV3Reques return nil } -func (client Client) CallGetSingleRawSecretByNameV3(request GetSingleSecretByNameV3Request) (GetSingleRawSecretByNameSecretResponse, error) { +func (client Client) GetSingleRawSecretByNameV3(request GetSingleSecretByNameV3Request) (GetSingleRawSecretByNameSecretResponse, error) { var secretsResponse GetSingleRawSecretByNameSecretResponse response, err := client.Config.HttpClient. R(). @@ -269,88 +228,149 @@ func (client Client) CallGetSingleRawSecretByNameV3(request GetSingleSecretByNam return secretsResponse, nil } -func (client Client) CallCreateProject(request CreateProjectRequest) (CreateProjectResponse, error) { - - if request.Slug == "" { - request = CreateProjectRequest{ - ProjectName: request.ProjectName, - OrganizationSlug: request.OrganizationSlug, - } +func (client Client) GetPlainTextSecretsViaServiceToken(secretFolderPath string, envSlug string) ([]SingleEnvironmentVariable, *GetServiceTokenDetailsResponse, error) { + if client.Config.ServiceToken == "" { + return nil, nil, fmt.Errorf("service token must be defined to fetch secrets") } - var projectResponse CreateProjectResponse - response, err := client.Config.HttpClient. - R(). - SetResult(&projectResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post("api/v2/workspace") + serviceTokenParts := strings.SplitN(client.Config.ServiceToken, ".", 4) + if len(serviceTokenParts) < 4 { + return nil, nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") + } + serviceTokenDetails, err := client.GetServiceTokenDetailsV2() if err != nil { - return CreateProjectResponse{}, fmt.Errorf("CallCreateProject: Unable to complete api request [err=%s]", err) + return nil, nil, fmt.Errorf("unable to get service token details. [err=%v]", err) } - if response.IsError() { - return CreateProjectResponse{}, fmt.Errorf("CallCreateProject: Unsuccessful response. [response=%s]", response) + request := GetEncryptedSecretsV3Request{ + WorkspaceId: serviceTokenDetails.Workspace, + Environment: envSlug, } - return projectResponse, nil -} + if secretFolderPath != "" { + request.SecretPath = secretFolderPath + } -func (client Client) CallDeleteProject(request DeleteProjectRequest) error { - var projectResponse DeleteProjectResponse - response, err := client.Config.HttpClient. - R(). - SetResult(&projectResponse). - SetHeader("User-Agent", USER_AGENT). - Delete(fmt.Sprintf("api/v2/workspace/%s", request.Slug)) + encryptedSecrets, err := client.GetSecretsV3(request) if err != nil { - return fmt.Errorf("CallDeleteProject: Unable to complete api request [err=%s]", err) + return nil, nil, err } - if response.IsError() { - return fmt.Errorf("CallDeleteProject: Unsuccessful response. [response=%s]", response) + decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag) + if err != nil { + return nil, nil, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) } - return nil -} - -func (client Client) CallGetProject(request GetProjectRequest) (ProjectWithEnvironments, error) { - var projectResponse ProjectWithEnvironments - response, err := client.Config.HttpClient. - R(). - SetResult(&projectResponse). - SetHeader("User-Agent", USER_AGENT). - Get(fmt.Sprintf("api/v2/workspace/%s", request.Slug)) - + plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) if err != nil { - return ProjectWithEnvironments{}, fmt.Errorf("CallGetProject: Unable to complete api request [err=%s]", err) + return nil, nil, fmt.Errorf("unable to decrypt the required workspace key") } - if response.IsError() { - return ProjectWithEnvironments{}, fmt.Errorf("CallGetProject: Unsuccessful response. [response=%s]", response) + plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) + if err != nil { + return nil, nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) } - return projectResponse, nil + return plainTextSecrets, &serviceTokenDetails, nil } -func (client Client) CallUpdateProject(request UpdateProjectRequest) (UpdateProjectResponse, error) { - var projectResponse UpdateProjectResponse - response, err := client.Config.HttpClient. - R(). - SetResult(&projectResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Patch(fmt.Sprintf("api/v2/workspace/%s", request.Slug)) +func (client Client) GetRawSecrets(secretFolderPath string, envSlug string, workspaceId string) ([]RawV3Secret, error) { + if client.Config.ClientId == "" || client.Config.ClientSecret == "" { + return nil, fmt.Errorf("client ID and client secret must be defined to fetch secrets with machine identity") + } + + request := GetRawSecretsV3Request{ + Environment: envSlug, + WorkspaceId: workspaceId, + } + + if secretFolderPath != "" { + request.SecretPath = secretFolderPath + } + + secrets, err := client.GetSecretsRawV3(request) if err != nil { - return UpdateProjectResponse{}, fmt.Errorf("CallUpdateProject: Unable to complete api request [err=%s]", err) + return nil, err } - if response.IsError() { - return UpdateProjectResponse{}, fmt.Errorf("CallUpdateProject: Unsuccessful response. [response=%s]", response) + return secrets.Secrets, nil + +} + +func GetPlainTextSecrets(key []byte, encryptedSecrets GetEncryptedSecretsV3Response) ([]SingleEnvironmentVariable, error) { + plainTextSecrets := []SingleEnvironmentVariable{} + for _, secret := range encryptedSecrets.Secrets { + // Decrypt key + key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV) + if err != nil { + return nil, fmt.Errorf("unable to decode secret IV for secret key") + } + + key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag) + if err != nil { + return nil, fmt.Errorf("unable to decode secret authentication tag for secret key") + } + + key_ciphertext, err := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext) + if err != nil { + return nil, fmt.Errorf("unable to decode secret cipher text for secret key") + } + + plainTextKey, err := crypto.DecryptSymmetric(key, key_ciphertext, key_tag, key_iv) + if err != nil { + return nil, fmt.Errorf("unable to symmetrically decrypt secret key") + } + + // Decrypt value + value_iv, err := base64.StdEncoding.DecodeString(secret.SecretValueIV) + if err != nil { + return nil, fmt.Errorf("unable to decode secret IV for secret value") + } + + value_tag, err := base64.StdEncoding.DecodeString(secret.SecretValueTag) + if err != nil { + return nil, fmt.Errorf("unable to decode secret authentication tag for secret value") + } + + value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext) + + plainTextValue, err := crypto.DecryptSymmetric(key, value_ciphertext, value_tag, value_iv) + if err != nil { + return nil, fmt.Errorf("unable to symmetrically decrypt secret value") + } + + // Decrypt comment + comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV) + if err != nil { + return nil, fmt.Errorf("unable to decode secret IV for secret value") + } + + comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag) + if err != nil { + return nil, fmt.Errorf("unable to decode secret authentication tag for secret value") + } + + comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext) + + plainTextComment, err := crypto.DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv) + if err != nil { + return nil, fmt.Errorf("unable to symmetrically decrypt secret comment") + } + + plainTextSecret := SingleEnvironmentVariable{ + Key: string(plainTextKey), + Value: string(plainTextValue), + Type: secret.Type, + ID: secret.ID, + Tags: secret.Tags, + Comment: string(plainTextComment), + } + + plainTextSecrets = append(plainTextSecrets, plainTextSecret) } - return projectResponse, nil + return plainTextSecrets, nil } diff --git a/client/crypto.go b/internal/crypto/crypto.go similarity index 89% rename from client/crypto.go rename to internal/crypto/crypto.go index 730f6f0..6a7a2c1 100644 --- a/client/crypto.go +++ b/internal/crypto/crypto.go @@ -1,4 +1,4 @@ -package infisicalclient +package crypto import ( "crypto/aes" @@ -9,9 +9,9 @@ import ( "golang.org/x/crypto/nacl/box" ) -// will decrypt cipher text to plain text using iv and tag +// will decrypt cipher text to plain text using iv and tag. func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) { - // Case: empty string + // Case: empty string. if len(cipherText) == 0 && len(tag) == 0 && len(iv) == 0 { return []byte{}, nil } @@ -27,7 +27,7 @@ func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]b } var nonce = iv - var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text + var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text. plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) if err != nil { @@ -38,12 +38,12 @@ func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]b } func GenerateNewKey() (newKey []byte, keyErr error) { - key := make([]byte, 16) // block size defaults to 16 so this is fine + key := make([]byte, 16) // block size defaults to 16 so this is fine. _, err := rand.Read(key) return key, err } -// Will encrypt a plain text with the provided key +// Will encrypt a plain text with the provided key. func EncryptSymmetric(plaintext []byte, key []byte) (result SymmetricEncryptionResult, err error) { block, err := aes.NewCipher(key) if err != nil { @@ -63,7 +63,7 @@ func EncryptSymmetric(plaintext []byte, key []byte) (result SymmetricEncryptionR ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) - ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it + ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it. authTag := ciphertext[len(ciphertext)-16:] diff --git a/internal/crypto/model.go b/internal/crypto/model.go new file mode 100644 index 0000000..55533bd --- /dev/null +++ b/internal/crypto/model.go @@ -0,0 +1,7 @@ +package crypto + +type SymmetricEncryptionResult struct { + CipherText []byte `json:"CipherText"` + Nonce []byte `json:"Nonce"` + AuthTag []byte `json:"AuthTag"` +} diff --git a/infisical/provider/projects_data_source.go b/internal/provider/datasource/projects_data_source.go similarity index 97% rename from infisical/provider/projects_data_source.go rename to internal/provider/datasource/projects_data_source.go index 1c2bc07..d9b1d80 100644 --- a/infisical/provider/projects_data_source.go +++ b/internal/provider/datasource/projects_data_source.go @@ -1,13 +1,13 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package provider +package datasource import ( "context" "fmt" - infisical "terraform-provider-infisical/client" + infisical "terraform-provider-infisical/internal/client" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -162,7 +162,7 @@ func (d *ProjectsDataSource) Read(ctx context.Context, req datasource.ReadReques return } - project, err := d.client.CallGetProject(infisical.GetProjectRequest{ + project, err := d.client.GetProject(infisical.GetProjectRequest{ Slug: data.Slug.ValueString(), }) if err != nil { diff --git a/infisical/provider/secrets_data_source.go b/internal/provider/datasource/secrets_data_source.go similarity index 98% rename from infisical/provider/secrets_data_source.go rename to internal/provider/datasource/secrets_data_source.go index 03982e1..63092e0 100644 --- a/infisical/provider/secrets_data_source.go +++ b/internal/provider/datasource/secrets_data_source.go @@ -1,13 +1,13 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package provider +package datasource import ( "context" "fmt" - infisical "terraform-provider-infisical/client" + infisical "terraform-provider-infisical/internal/client" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -73,6 +73,7 @@ func (d *SecretsDataSource) Schema(ctx context.Context, req datasource.SchemaReq "value": schema.StringAttribute{ Computed: true, Description: "The secret value", + Sensitive: true, }, "comment": schema.StringAttribute{ Computed: true, diff --git a/infisical/provider/provider.go b/internal/provider/provider.go similarity index 89% rename from infisical/provider/provider.go rename to internal/provider/provider.go index e77f3ab..dfe4d03 100644 --- a/infisical/provider/provider.go +++ b/internal/provider/provider.go @@ -4,7 +4,9 @@ import ( "context" "os" - infisical "terraform-provider-infisical/client" + infisical "terraform-provider-infisical/internal/client" + infisicalDatasource "terraform-provider-infisical/internal/provider/datasource" + infisicalResource "terraform-provider-infisical/internal/provider/resource" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -149,15 +151,19 @@ func (p *infisicalProvider) Configure(ctx context.Context, req provider.Configur // DataSources defines the data sources implemented in the provider. func (p *infisicalProvider) DataSources(_ context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ - NewSecretDataSource, - NewProjectDataSource, + infisicalDatasource.NewSecretDataSource, + infisicalDatasource.NewProjectDataSource, } } // Resources defines the resources implemented in the provider. func (p *infisicalProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ - NewSecretResource, - NewProjectResource, + infisicalResource.NewSecretResource, + infisicalResource.NewProjectResource, + infisicalResource.NewProjectUserResource, + infisicalResource.NewProjectIdentityResource, + infisicalResource.NewProjectRoleResource, + infisicalResource.NewProjectIdentitySpecificPrivilegeResource, } } diff --git a/internal/provider/resource/project_identity_resource.go b/internal/provider/resource/project_identity_resource.go new file mode 100644 index 0000000..2f00e66 --- /dev/null +++ b/internal/provider/resource/project_identity_resource.go @@ -0,0 +1,557 @@ +package resource + +import ( + "context" + "fmt" + infisical "terraform-provider-infisical/internal/client" + "time" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &ProjectIdentityResource{} +) + +// NewProjectResource is a helper function to simplify the provider implementation. +func NewProjectIdentityResource() resource.Resource { + return &ProjectIdentityResource{} +} + +// ProjectIdentityResource is the resource implementation. +type ProjectIdentityResource struct { + client *infisical.Client +} + +// projectResourceSourceModel describes the data source data model. +type ProjectIdentityResourceModel struct { + ProjectID types.String `tfsdk:"project_id"` + IdentityID types.String `tfsdk:"identity_id"` + Identity types.Object `tfsdk:"identity"` + Roles []ProjectIdentityRole `tfsdk:"roles"` + MembershipId types.String `tfsdk:"membership_id"` +} + +type ProjectIdentityDetails struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + AuthMethod types.String `tfsdk:"auth_method"` +} + +type ProjectIdentityRole struct { + ID types.String `tfsdk:"id"` + RoleSlug types.String `tfsdk:"role_slug"` + CustomRoleID types.String `tfsdk:"custom_role_id"` + IsTemporary types.Bool `tfsdk:"is_temporary"` + TemporaryMode types.String `tfsdk:"temporary_mode"` + TemporaryRange types.String `tfsdk:"temporary_range"` + TemporaryAccesStartTime types.String `tfsdk:"temporary_access_start_time"` + TemporaryAccessEndTime types.String `tfsdk:"temporary_access_end_time"` +} + +// Metadata returns the resource type name. +func (r *ProjectIdentityResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_identity" +} + +// Schema defines the schema for the resource. +func (r *ProjectIdentityResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Create project identities & save to Infisical. Only Machine Identity authentication is supported for this data source", + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "The id of the project", + Required: true, + }, + "identity_id": schema.StringAttribute{ + Description: "The id of the identity.", + Required: true, + }, + "membership_id": schema.StringAttribute{ + Description: "The membership Id of the project identity", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "identity": schema.SingleNestedAttribute{ + Computed: true, + Description: "The identity details of the project identity", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the identity", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "name": schema.StringAttribute{ + Description: "The name of the identity", + Computed: true, + }, + "auth_method": schema.StringAttribute{ + Description: "The auth method for the identity", + Computed: true, + }, + }, + }, + "roles": schema.ListNestedAttribute{ + Required: true, + Description: "The roles assigned to the project identity", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the project identity role.", + Computed: true, + }, + "role_slug": schema.StringAttribute{ + Description: "The slug of the role", + Required: true, + }, + "custom_role_id": schema.StringAttribute{ + Description: "The id of the custom role slug", + Computed: true, + Optional: true, + }, + "is_temporary": schema.BoolAttribute{ + Description: "Flag to indicate the assigned role is temporary or not. When is_temporary is true fields temporary_mode, temporary_range and temporary_access_start_time is required.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "temporary_mode": schema.StringAttribute{ + Description: "Type of temporary access given. Types: relative. Default: relative", + Optional: true, + Computed: true, + }, + "temporary_range": schema.StringAttribute{ + Description: "TTL for the temporary time. Eg: 1m, 1h, 1d. Default: 1h", + Optional: true, + Computed: true, + }, + "temporary_access_start_time": schema.StringAttribute{ + Description: "ISO time for which temporary access should begin. The current time is used by default.", + Optional: true, + Computed: true, + }, + "temporary_access_end_time": schema.StringAttribute{ + Description: "ISO time for which temporary access will end. Computed based on temporary_range and temporary_access_start_time", + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *ProjectIdentityResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*infisical.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Source Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +// Create creates the resource and sets the initial Terraform state. +func (r *ProjectIdentityResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to create project identity", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan ProjectIdentityResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var roles []infisical.CreateProjectIdentityRequestRoles + var hasAtleastOnePermanentRole bool + for _, el := range plan.Roles { + isTemporary := el.IsTemporary.ValueBool() + temporaryMode := el.TemporaryMode.ValueString() + temporaryRange := el.TemporaryRange.ValueString() + temporaryAccesStartTime := time.Now().UTC() + + if !isTemporary { + hasAtleastOnePermanentRole = true + } + + if el.TemporaryAccesStartTime.ValueString() != "" { + var err error + temporaryAccesStartTime, err = time.Parse(time.RFC3339, el.TemporaryAccesStartTime.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing field TemporaryAccessStartTime", + fmt.Sprintf("Must provider valid ISO timestamp for field temporaryAccesStartTime %s, role %s", el.TemporaryAccesStartTime.ValueString(), el.RoleSlug.ValueString()), + ) + return + } + } + + // default values + if isTemporary && temporaryMode == "" { + temporaryMode = TEMPORARY_MODE_RELATIVE + } + if isTemporary && temporaryRange == "" { + temporaryRange = "1h" + } + + roles = append(roles, infisical.CreateProjectIdentityRequestRoles{ + Role: el.RoleSlug.ValueString(), + IsTemporary: isTemporary, + TemporaryMode: temporaryMode, + TemporaryRange: temporaryRange, + TemporaryAccessStartTime: temporaryAccesStartTime, + }) + } + if !hasAtleastOnePermanentRole { + resp.Diagnostics.AddError("Error assigning role to identity", "Must have atleast one permanent role") + return + } + + _, err := r.client.CreateProjectIdentity(infisical.CreateProjectIdentityRequest{ + ProjectID: plan.ProjectID.ValueString(), + IdentityID: plan.IdentityID.ValueString(), + Roles: roles, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error attaching identity to project", + "Couldn't create project identity to Infiscial, unexpected error: "+err.Error(), + ) + return + } + + projectIdentityDetails, err := r.client.GetProjectIdentityByID(infisical.GetProjectIdentityByIDRequest{ + ProjectID: plan.ProjectID.ValueString(), + IdentityID: plan.IdentityID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error fetching identity", + "Couldn't find identity in project, unexpected error: "+err.Error(), + ) + return + } + + planRoles := make([]ProjectIdentityRole, 0, len(projectIdentityDetails.Membership.Roles)) + for _, el := range projectIdentityDetails.Membership.Roles { + val := ProjectIdentityRole{ + ID: types.StringValue(el.ID), + RoleSlug: types.StringValue(el.Role), + TemporaryAccessEndTime: types.StringValue(el.TemporaryAccessEndTime.Format(time.RFC3339)), + TemporaryRange: types.StringValue(el.TemporaryRange), + TemporaryMode: types.StringValue(el.TemporaryMode), + CustomRoleID: types.StringValue(el.CustomRoleId), + IsTemporary: types.BoolValue(el.IsTemporary), + TemporaryAccesStartTime: types.StringValue(el.TemporaryAccessStartTime.Format(time.RFC3339)), + } + if el.CustomRoleId != "" { + val.RoleSlug = types.StringValue(el.CustomRoleSlug) + } + + if !el.IsTemporary { + val.TemporaryMode = types.StringNull() + val.TemporaryRange = types.StringNull() + val.TemporaryAccesStartTime = types.StringNull() + val.TemporaryAccessEndTime = types.StringNull() + } + planRoles = append(planRoles, val) + } + plan.Roles = planRoles + plan.MembershipId = types.StringValue(projectIdentityDetails.Membership.ID) + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + identityDetails := ProjectIdentityDetails{ + ID: types.StringValue(projectIdentityDetails.Membership.Identity.Id), + Name: types.StringValue(projectIdentityDetails.Membership.Identity.Name), + AuthMethod: types.StringValue(projectIdentityDetails.Membership.Identity.AuthMethod), + } + diags = resp.State.SetAttribute(ctx, path.Root("identity"), identityDetails) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Read refreshes the Terraform state with the latest data. +func (r *ProjectIdentityResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to read project identity", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Get current state + var state ProjectIdentityResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectIdentityDetails, err := r.client.GetProjectIdentityByID(infisical.GetProjectIdentityByIDRequest{ + ProjectID: state.ProjectID.ValueString(), + IdentityID: state.IdentityID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error fetching identity", + "Couldn't find identity in project, unexpected error: "+err.Error(), + ) + return + } + planRoles := make([]ProjectIdentityRole, 0, len(projectIdentityDetails.Membership.Roles)) + for _, el := range projectIdentityDetails.Membership.Roles { + val := ProjectIdentityRole{ + ID: types.StringValue(el.ID), + RoleSlug: types.StringValue(el.Role), + TemporaryAccessEndTime: types.StringValue(el.TemporaryAccessEndTime.Format(time.RFC3339)), + TemporaryRange: types.StringValue(el.TemporaryRange), + TemporaryMode: types.StringValue(el.TemporaryMode), + CustomRoleID: types.StringValue(el.CustomRoleId), + IsTemporary: types.BoolValue(el.IsTemporary), + TemporaryAccesStartTime: types.StringValue(el.TemporaryAccessStartTime.Format(time.RFC3339)), + } + if el.CustomRoleId != "" { + val.RoleSlug = types.StringValue(el.CustomRoleSlug) + } + if !el.IsTemporary { + val.TemporaryMode = types.StringNull() + val.TemporaryRange = types.StringNull() + val.TemporaryAccesStartTime = types.StringNull() + val.TemporaryAccessEndTime = types.StringNull() + } + planRoles = append(planRoles, val) + } + + state.Roles = planRoles + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + identityDetails := ProjectIdentityDetails{ + ID: types.StringValue(projectIdentityDetails.Membership.Identity.Id), + Name: types.StringValue(projectIdentityDetails.Membership.Identity.Name), + AuthMethod: types.StringValue(projectIdentityDetails.Membership.Identity.AuthMethod), + } + diags = resp.State.SetAttribute(ctx, path.Root("identity"), identityDetails) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *ProjectIdentityResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to update project identity", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan ProjectIdentityResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var state ProjectIdentityResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if state.IdentityID != plan.IdentityID { + resp.Diagnostics.AddError( + "Unable to update project identity", + fmt.Sprintf("Cannot change identity id, previous identity: %s, new identity id: %s", state.IdentityID, plan.IdentityID), + ) + return + } + + var roles []infisical.UpdateProjectIdentityRequestRoles + var hasAtleastOnePermanentRole bool + for _, el := range plan.Roles { + isTemporary := el.IsTemporary.ValueBool() + temporaryMode := el.TemporaryMode.ValueString() + temporaryRange := el.TemporaryRange.ValueString() + temporaryAccesStartTime := time.Now().UTC() + + if !isTemporary { + hasAtleastOnePermanentRole = true + } + + if el.TemporaryAccesStartTime.ValueString() != "" { + var err error + temporaryAccesStartTime, err = time.Parse(time.RFC3339, el.TemporaryAccesStartTime.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing field TemporaryAccessStartTime", + fmt.Sprintf("Must provider valid ISO timestamp for field temporaryAccesStartTime %s, role %s", el.TemporaryAccesStartTime.ValueString(), el.RoleSlug.ValueString()), + ) + return + } + } + + // default values + if isTemporary && temporaryMode == "" { + temporaryMode = TEMPORARY_MODE_RELATIVE + } + if isTemporary && temporaryRange == "" { + temporaryRange = "1h" + } + + roles = append(roles, infisical.UpdateProjectIdentityRequestRoles{ + Role: el.RoleSlug.ValueString(), + IsTemporary: isTemporary, + TemporaryMode: temporaryMode, + TemporaryRange: temporaryRange, + TemporaryAccessStartTime: temporaryAccesStartTime, + }) + } + + if !hasAtleastOnePermanentRole { + resp.Diagnostics.AddError("Error assigning role to identity", "Must have atleast one permanent role") + return + } + + _, err := r.client.UpdateProjectIdentity(infisical.UpdateProjectIdentityRequest{ + ProjectID: plan.ProjectID.ValueString(), + IdentityID: plan.IdentityID.ValueString(), + Roles: roles, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error assigning roles to identity", + "Couldn't update role , unexpected error: "+err.Error(), + ) + return + } + + projectIdentityDetails, err := r.client.GetProjectIdentityByID(infisical.GetProjectIdentityByIDRequest{ + ProjectID: plan.ProjectID.ValueString(), + IdentityID: plan.IdentityID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error fetching identity", + "Couldn't find identity in project, unexpected error: "+err.Error(), + ) + return + } + + planRoles := make([]ProjectIdentityRole, 0, len(projectIdentityDetails.Membership.Roles)) + for _, el := range projectIdentityDetails.Membership.Roles { + val := ProjectIdentityRole{ + ID: types.StringValue(el.ID), + RoleSlug: types.StringValue(el.Role), + TemporaryAccessEndTime: types.StringValue(el.TemporaryAccessEndTime.Format(time.RFC3339)), + TemporaryRange: types.StringValue(el.TemporaryRange), + TemporaryMode: types.StringValue(el.TemporaryMode), + CustomRoleID: types.StringValue(el.CustomRoleId), + IsTemporary: types.BoolValue(el.IsTemporary), + TemporaryAccesStartTime: types.StringValue(el.TemporaryAccessStartTime.Format(time.RFC3339)), + } + + if el.CustomRoleId != "" { + val.RoleSlug = types.StringValue(el.CustomRoleSlug) + } + + if !el.IsTemporary { + val.TemporaryMode = types.StringNull() + val.TemporaryRange = types.StringNull() + val.TemporaryAccesStartTime = types.StringNull() + val.TemporaryAccessEndTime = types.StringNull() + } + planRoles = append(planRoles, val) + } + plan.Roles = planRoles + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + identityDetails := ProjectIdentityDetails{ + ID: types.StringValue(projectIdentityDetails.Membership.Identity.Id), + Name: types.StringValue(projectIdentityDetails.Membership.Identity.Name), + AuthMethod: types.StringValue(projectIdentityDetails.Membership.Identity.AuthMethod), + } + diags = resp.State.SetAttribute(ctx, path.Root("identity"), identityDetails) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *ProjectIdentityResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to delete project identity", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + var state ProjectIdentityResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.DeleteProjectIdentity(infisical.DeleteProjectIdentityRequest{ + ProjectID: state.ProjectID.ValueString(), + IdentityID: state.IdentityID.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project identity", + "Couldn't delete project identity from Infiscial, unexpected error: "+err.Error(), + ) + return + } + +} diff --git a/internal/provider/resource/project_identity_specific_privilege.go b/internal/provider/resource/project_identity_specific_privilege.go new file mode 100644 index 0000000..5feab73 --- /dev/null +++ b/internal/provider/resource/project_identity_specific_privilege.go @@ -0,0 +1,576 @@ +package resource + +import ( + "context" + "fmt" + "strings" + infisical "terraform-provider-infisical/internal/client" + infisicalclient "terraform-provider-infisical/internal/client" + "time" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &projectIdentitySpecificPrivilegeResourceResource{} + SPECIFIC_PRIVILEGE_PERMISSION_ACTIONS = []string{"create", "edit", "delete", "read"} + SPECIFIC_PRIVILEGE_PERMISSION_SUBJECTS = []string{"secrets"} +) + +// NewProjectResource is a helper function to simplify the provider implementation. +func NewProjectIdentitySpecificPrivilegeResource() resource.Resource { + return &projectIdentitySpecificPrivilegeResourceResource{} +} + +// projectIdentitySpecificPrivilegeResourceResource is the resource implementation. +type projectIdentitySpecificPrivilegeResourceResource struct { + client *infisical.Client +} + +// projectIdentitySpecificPrivilegeResourceResourceSourceModel describes the data source data model. +type projectIdentitySpecificPrivilegeResourceResourceModel struct { + Slug types.String `tfsdk:"slug"` + ProjectSlug types.String `tfsdk:"project_slug"` + IdentityID types.String `tfsdk:"identity_id"` + ID types.String `tfsdk:"id"` + Permission projectIdentitySpecificPrivilegeResourceResourcePermissions `tfsdk:"permission"` + IsTemporary types.Bool `tfsdk:"is_temporary"` + TemporaryMode types.String `tfsdk:"temporary_mode"` + TemporaryRange types.String `tfsdk:"temporary_range"` + TemporaryAccesStartTime types.String `tfsdk:"temporary_access_start_time"` + TemporaryAccessEndTime types.String `tfsdk:"temporary_access_end_time"` +} + +type projectIdentitySpecificPrivilegeResourceResourcePermissions struct { + Actions types.List `tfsdk:"actions"` + Subject types.String `tfsdk:"subject"` + Conditions projectIdentitySpecificPrivilegeResourceResourcePermissionCondition `tfsdk:"conditions"` +} + +type projectIdentitySpecificPrivilegeResourceResourcePermissionCondition struct { + Environment types.String `tfsdk:"environment"` + SecretPath types.String `tfsdk:"secret_path"` +} + +// Metadata returns the resource type name. +func (r *projectIdentitySpecificPrivilegeResourceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_identity_specific_privilege" +} + +// Schema defines the schema for the resource. +func (r *projectIdentitySpecificPrivilegeResourceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Create additional privileges for identities & save to Infisical. Only Machine Identity authentication is supported for this data source.", + Attributes: map[string]schema.Attribute{ + "identity_id": schema.StringAttribute{ + Description: "The identity id to create identity specific privilege", + Required: true, + }, + "project_slug": schema.StringAttribute{ + Description: "The slug of the project to create identity specific privilege", + Required: true, + }, + "slug": schema.StringAttribute{ + Description: "The slug for the new privilege", + Optional: true, + Computed: true, + }, + "id": schema.StringAttribute{ + Description: "The ID of the privilege", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "is_temporary": schema.BoolAttribute{ + Description: "Flag to indicate the assigned specific privilege is temporary or not. When is_temporary is true fields temporary_mode, temporary_range and temporary_access_start_time is required.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "temporary_mode": schema.StringAttribute{ + Description: "Type of temporary access given. Types: relative. Default: relative", + Optional: true, + Computed: true, + }, + "temporary_range": schema.StringAttribute{ + Description: "TTL for the temporary time. Eg: 1m, 1h, 1d. Default: 1h", + Optional: true, + Computed: true, + }, + "temporary_access_start_time": schema.StringAttribute{ + Description: "ISO time for which temporary access should begin. The current time is used by default.", + Optional: true, + Computed: true, + }, + "temporary_access_end_time": schema.StringAttribute{ + Description: "ISO time for which temporary access will end. Computed based on temporary_range and temporary_access_start_time", + Computed: true, + Optional: true, + }, + "permission": schema.SingleNestedAttribute{ + Required: true, + Description: "The permissions assigned to the project identity specific privilege", + Attributes: map[string]schema.Attribute{ + "actions": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + Description: fmt.Sprintf("Describe what action an entity can take. Enum: %s", strings.Join(PERMISSION_ACTIONS, ",")), + }, + "subject": schema.StringAttribute{ + Description: fmt.Sprintf("Describe what action an entity can take. Enum: %s", strings.Join(PERMISSION_SUBJECTS, ",")), + Required: true, + }, + "conditions": schema.SingleNestedAttribute{ + Required: true, + Description: "The conditions to scope permissions", + Attributes: map[string]schema.Attribute{ + "environment": schema.StringAttribute{ + Description: "The environment slug this permission should allow.", + Required: true, + }, + "secret_path": schema.StringAttribute{ + Description: "The secret path this permission should be scoped to", + Optional: true, + }, + }, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *projectIdentitySpecificPrivilegeResourceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*infisical.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +// Create creates the resource and sets the initial Terraform state. +func (r *projectIdentitySpecificPrivilegeResourceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to create project identity specific privilege", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan projectIdentitySpecificPrivilegeResourceResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + planPermissionActions := make([]types.String, 0, len(plan.Permission.Actions.Elements())) + diags = plan.Permission.Actions.ElementsAs(ctx, &planPermissionActions, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + condition := make(map[string]any) + environment := plan.Permission.Conditions.Environment.ValueString() + secretPath := plan.Permission.Conditions.SecretPath.ValueString() + condition["environment"] = environment + if secretPath != "" { + condition["secretPath"] = map[string]string{"$glob": secretPath} + } + + actions := make([]string, 0, len(planPermissionActions)) + for _, action := range planPermissionActions { + actions = append(actions, action.ValueString()) + } + privilegePermission := infisicalclient.ProjectSpecificPrivilegePermissionRequest{ + Actions: actions, + Subject: plan.Permission.Subject.ValueString(), + Conditions: condition, + } + + if plan.IsTemporary.ValueBool() { + temporaryMode := plan.TemporaryMode.ValueString() + temporaryRange := plan.TemporaryRange.ValueString() + temporaryAccesStartTime := time.Now().UTC() + + if plan.TemporaryAccesStartTime.ValueString() != "" { + var err error + temporaryAccesStartTime, err = time.Parse(time.RFC3339, plan.TemporaryAccesStartTime.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing field TemporaryAccessStartTime", + fmt.Sprintf("Must provider valid ISO timestamp for field temporaryAccesStartTime %s", plan.TemporaryAccesStartTime.ValueString()), + ) + return + } + } + + // default values + if temporaryMode == "" { + temporaryMode = TEMPORARY_MODE_RELATIVE + } + if temporaryRange == "" { + temporaryRange = "1h" + } + + newProjectRole, err := r.client.CreateTemporaryProjectIdentitySpecificPrivilege(infisical.CreateTemporaryProjectIdentitySpecificPrivilegeRequest{ + ProjectSlug: plan.ProjectSlug.ValueString(), + Slug: plan.Slug.ValueString(), + IdentityId: plan.IdentityID.ValueString(), + Permissions: privilegePermission, + TemporaryMode: temporaryMode, + TemporaryRange: temporaryRange, + TemporaryAccessStartTime: temporaryAccesStartTime, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project identity specific privilege", + "Couldn't save project identity specific privilege to Infiscial, unexpected error: "+err.Error(), + ) + return + } + + plan.ID = types.StringValue(newProjectRole.Privilege.ID) + plan.TemporaryAccessEndTime = types.StringValue(newProjectRole.Privilege.TemporaryAccessEndTime.Format(time.RFC3339)) + plan.TemporaryAccesStartTime = types.StringValue(newProjectRole.Privilege.TemporaryAccessStartTime.Format(time.RFC3339)) + plan.Slug = types.StringValue(newProjectRole.Privilege.Slug) + plan.TemporaryRange = types.StringValue(newProjectRole.Privilege.TemporaryRange) + plan.TemporaryMode = types.StringValue(newProjectRole.Privilege.TemporaryMode) + } else { + newProjectRole, err := r.client.CreatePermanentProjectIdentitySpecificPrivilege(infisical.CreatePermanentProjectIdentitySpecificPrivilegeRequest{ + ProjectSlug: plan.ProjectSlug.ValueString(), + Slug: plan.Slug.ValueString(), + IdentityId: plan.IdentityID.ValueString(), + Permissions: privilegePermission, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project identity specific privilege", + "Couldn't save project identity specific privilege to Infiscial, unexpected error: "+err.Error(), + ) + return + } + + plan.ID = types.StringValue(newProjectRole.Privilege.ID) + plan.Slug = types.StringValue(newProjectRole.Privilege.Slug) + plan.TemporaryAccessEndTime = types.StringNull() + plan.TemporaryAccesStartTime = types.StringNull() + plan.TemporaryRange = types.StringNull() + plan.TemporaryMode = types.StringNull() + } + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Read refreshes the Terraform state with the latest data. +func (r *projectIdentitySpecificPrivilegeResourceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to read project identity specific privilege", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Get current state + var state projectIdentitySpecificPrivilegeResourceResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the latest data from the API + projectIdentitySpecificPrivilegeResource, err := r.client.GetProjectIdentitySpecificPrivilegeBySlug(infisical.GetProjectIdentitySpecificPrivilegeRequest{ + PrivilegeSlug: state.Slug.ValueString(), + ProjectSlug: state.ProjectSlug.ValueString(), + IdentityId: state.IdentityID.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error reading project identity specific privilege", + "Couldn't read project identity specific privilege from Infiscial, unexpected error: "+err.Error(), + ) + return + } + + state.ID = types.StringValue(projectIdentitySpecificPrivilegeResource.Privilege.ID) + if projectIdentitySpecificPrivilegeResource.Privilege.IsTemporary { + state.TemporaryAccessEndTime = types.StringValue(projectIdentitySpecificPrivilegeResource.Privilege.TemporaryAccessEndTime.Format(time.RFC3339)) + state.TemporaryAccesStartTime = types.StringValue(projectIdentitySpecificPrivilegeResource.Privilege.TemporaryAccessStartTime.Format(time.RFC3339)) + state.TemporaryRange = types.StringValue(projectIdentitySpecificPrivilegeResource.Privilege.TemporaryRange) + state.TemporaryMode = types.StringValue(projectIdentitySpecificPrivilegeResource.Privilege.TemporaryMode) + } else { + state.TemporaryAccessEndTime = types.StringNull() + state.TemporaryAccesStartTime = types.StringNull() + state.TemporaryRange = types.StringNull() + state.TemporaryMode = types.StringNull() + } + + planPermissionActions := make([]attr.Value, 0, len(projectIdentitySpecificPrivilegeResource.Privilege.Permissions)) + var planPermissionSubject, planPermissionEnvironment, planPermissionSecretPath types.String + for _, el := range projectIdentitySpecificPrivilegeResource.Privilege.Permissions { + action, isValid := el["action"].(string) + if el["action"] != nil && !isValid { + action, isValid = el["action"].([]any)[0].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project identity specific privilege", + "Couldn't read project identity specific privilege from Infiscial, invalid action field in permission", + ) + return + } + } + + subject, isValid := el["subject"].(string) + if el["subject"] != nil && !isValid { + subject, isValid = el["subject"].([]any)[0].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project identity specific privilege", + "Couldn't read project identity specific privilege from Infiscial, invalid subject field in permission", + ) + return + } + } + + conditions, isValid := el["conditions"].(map[string]any) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project identity specific privilege", + "Couldn't read project identity specific privilege from Infiscial, invalid conditions field in permission", + ) + return + } + + planPermissionActions = append(planPermissionActions, types.StringValue(action)) + environment, isValid := conditions["environment"].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project identity specific privilege", + "Couldn't read project identity specific privilege from Infiscial, invalid environment field in permission", + ) + return + } + planPermissionEnvironment = types.StringValue(environment) + + planPermissionSubject = types.StringValue(subject) + if val, isValid := conditions["secretPath"].(map[string]any); isValid { + secretPath, isValid := val["$glob"].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project identity specific privilege", + "Couldn't read project identity specific privilege from Infiscial, invalid secret path field in permission", + ) + return + } + planPermissionSecretPath = types.StringValue(secretPath) + } + } + + stateAction, diags := basetypes.NewListValue(types.StringType, planPermissionActions) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + state.Permission = projectIdentitySpecificPrivilegeResourceResourcePermissions{ + Actions: stateAction, + Subject: planPermissionSubject, + Conditions: projectIdentitySpecificPrivilegeResourceResourcePermissionCondition{ + Environment: planPermissionEnvironment, + SecretPath: planPermissionSecretPath, + }, + } + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *projectIdentitySpecificPrivilegeResourceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to update project identity specific privilege", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan projectIdentitySpecificPrivilegeResourceResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var state projectIdentitySpecificPrivilegeResourceResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if state.ProjectSlug != plan.ProjectSlug { + resp.Diagnostics.AddError( + "Unable to update project identity specific privilege", + "Project slug cannot be updated", + ) + return + } + + planPermissionActions := make([]types.String, 0, len(plan.Permission.Actions.Elements())) + diags = plan.Permission.Actions.ElementsAs(ctx, &planPermissionActions, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + condition := make(map[string]any) + environment := plan.Permission.Conditions.Environment.ValueString() + secretPath := plan.Permission.Conditions.SecretPath.ValueString() + condition["environment"] = environment + if secretPath != "" { + condition["secretPath"] = map[string]string{"$glob": secretPath} + } + + actions := make([]string, 0, len(planPermissionActions)) + for _, action := range planPermissionActions { + actions = append(actions, action.ValueString()) + } + privilegePermission := infisicalclient.ProjectSpecificPrivilegePermissionRequest{ + Actions: actions, + Subject: plan.Permission.Subject.ValueString(), + Conditions: condition, + } + isTemporary := plan.IsTemporary.ValueBool() + temporaryMode := plan.TemporaryMode.ValueString() + temporaryRange := plan.TemporaryRange.ValueString() + temporaryAccesStartTime := time.Now().UTC() + + if plan.TemporaryAccesStartTime.ValueString() != "" { + var err error + temporaryAccesStartTime, err = time.Parse(time.RFC3339, plan.TemporaryAccesStartTime.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing field TemporaryAccessStartTime", + fmt.Sprintf("Must provider valid ISO timestamp for field temporaryAccesStartTime %s", plan.TemporaryAccesStartTime.ValueString()), + ) + return + } + } + + if isTemporary && temporaryMode == "" { + temporaryMode = TEMPORARY_MODE_RELATIVE + } + if isTemporary && temporaryRange == "" { + temporaryRange = "1h" + } + + updatedSpecificPrivilege, err := r.client.UpdateProjectIdentitySpecificPrivilege(infisical.UpdateProjectIdentitySpecificPrivilegeRequest{ + ProjectSlug: plan.ProjectSlug.ValueString(), + PrivilegeSlug: state.Slug.ValueString(), + IdentityId: plan.IdentityID.ValueString(), + Details: infisical.UpdateProjectIdentitySpecificPrivilegeDataRequest{ + Slug: plan.Slug.ValueString(), + Permissions: privilegePermission, + IsTemporary: isTemporary, + TemporaryMode: temporaryMode, + TemporaryRange: temporaryRange, + TemporaryAccessStartTime: temporaryAccesStartTime, + }, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error updating project identity specific privilege", + "Couldn't update project identity specific privilege from Infiscial, unexpected error: "+err.Error(), + ) + return + } + + plan.Slug = types.StringValue(updatedSpecificPrivilege.Privilege.Slug) + if updatedSpecificPrivilege.Privilege.IsTemporary { + plan.TemporaryAccessEndTime = types.StringValue(updatedSpecificPrivilege.Privilege.TemporaryAccessEndTime.Format(time.RFC3339)) + plan.TemporaryAccesStartTime = types.StringValue(updatedSpecificPrivilege.Privilege.TemporaryAccessStartTime.Format(time.RFC3339)) + plan.TemporaryRange = types.StringValue(updatedSpecificPrivilege.Privilege.TemporaryRange) + plan.TemporaryMode = types.StringValue(updatedSpecificPrivilege.Privilege.TemporaryMode) + } else { + plan.TemporaryAccessEndTime = types.StringNull() + plan.TemporaryAccesStartTime = types.StringNull() + plan.TemporaryRange = types.StringNull() + plan.TemporaryMode = types.StringNull() + } + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *projectIdentitySpecificPrivilegeResourceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to delete project identity specific privilege", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + var state projectIdentitySpecificPrivilegeResourceResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.DeleteProjectIdentitySpecificPrivilege(infisical.DeleteProjectIdentitySpecificPrivilegeRequest{ + ProjectSlug: state.ProjectSlug.ValueString(), + IdentityId: state.IdentityID.ValueString(), + PrivilegeSlug: state.Slug.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project identity specific privilege", + "Couldn't delete project identity specific privilege from Infiscial, unexpected error: "+err.Error(), + ) + return + } + +} diff --git a/infisical/provider/project_resource.go b/internal/provider/resource/project_resource.go similarity index 87% rename from infisical/provider/project_resource.go rename to internal/provider/resource/project_resource.go index 2b746ab..5c733b8 100644 --- a/infisical/provider/project_resource.go +++ b/internal/provider/resource/project_resource.go @@ -1,13 +1,15 @@ -package provider +package resource import ( "context" "fmt" - infisical "terraform-provider-infisical/client" + infisical "terraform-provider-infisical/internal/client" "time" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -28,9 +30,10 @@ type projectResource struct { // projectResourceSourceModel describes the data source data model. type projectResourceModel struct { - Slug types.String `tfsdk:"slug"` - Name types.String `tfsdk:"name"` - LastUpdated types.String `tfsdk:"last_updated"` + Slug types.String `tfsdk:"slug"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + LastUpdated types.String `tfsdk:"last_updated"` } // Metadata returns the resource type name. @@ -51,7 +54,11 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: "The name of the project", Required: true, }, - + "id": schema.StringAttribute{ + Description: "The ID of the project", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, "last_updated": schema.StringAttribute{ Computed: true, }, @@ -97,7 +104,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - _, err := r.client.CallCreateProject(infisical.CreateProjectRequest{ + newProject, err := r.client.CreateProject(infisical.CreateProjectRequest{ ProjectName: plan.Name.ValueString(), Slug: plan.Slug.ValueString(), }) @@ -113,6 +120,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) plan.Slug = types.StringValue(plan.Slug.ValueString()) plan.Name = types.StringValue(plan.Name.ValueString()) + plan.ID = types.StringValue(newProject.Project.ID) diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -141,7 +149,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re } // Get the latest data from the API - project, err := r.client.CallGetProject(infisical.GetProjectRequest{ + project, err := r.client.GetProject(infisical.GetProjectRequest{ Slug: state.Slug.ValueString(), }) @@ -198,7 +206,7 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest return } - _, err := r.client.CallUpdateProject(infisical.UpdateProjectRequest{ + _, err := r.client.UpdateProject(infisical.UpdateProjectRequest{ ProjectName: plan.Name.ValueString(), Slug: plan.Slug.ValueString(), }) @@ -240,7 +248,7 @@ func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest return } - err := r.client.CallDeleteProject(infisical.DeleteProjectRequest{ + err := r.client.DeleteProject(infisical.DeleteProjectRequest{ Slug: state.Slug.ValueString(), }) diff --git a/internal/provider/resource/project_role_resource.go b/internal/provider/resource/project_role_resource.go new file mode 100644 index 0000000..b24dade --- /dev/null +++ b/internal/provider/resource/project_role_resource.go @@ -0,0 +1,431 @@ +package resource + +import ( + "context" + "fmt" + "strings" + infisical "terraform-provider-infisical/internal/client" + infisicalclient "terraform-provider-infisical/internal/client" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &projectRoleResource{} + PERMISSION_ACTIONS = []string{"create", "edit", "delete", "read"} + PERMISSION_SUBJECTS = []string{"role", "member", "groups", "settings", "integrations", "webhooks", "service-tokens", "environments", "tags", "audit-logs", "ip-allowlist", "workspace", "secrets", "secret-rollback", "secret-approval", "secret-rotation", "identity"} +) + +// NewProjectResource is a helper function to simplify the provider implementation. +func NewProjectRoleResource() resource.Resource { + return &projectRoleResource{} +} + +// projectRoleResource is the resource implementation. +type projectRoleResource struct { + client *infisical.Client +} + +// projectRoleResourceSourceModel describes the data source data model. +type projectRoleResourceModel struct { + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Slug types.String `tfsdk:"slug"` + ProjectSlug types.String `tfsdk:"project_slug"` + ID types.String `tfsdk:"id"` + Permissions []projectRoleResourcePermissions `tfsdk:"permissions"` +} + +type projectRoleResourcePermissions struct { + Action types.String `tfsdk:"action"` + Subject types.String `tfsdk:"subject"` + Conditions *projectRoleResourcePermissionCondition `tfsdk:"conditions"` +} + +type projectRoleResourcePermissionCondition struct { + Environment types.String `tfsdk:"environment"` + SecretPath types.String `tfsdk:"secret_path"` +} + +// Metadata returns the resource type name. +func (r *projectRoleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_role" +} + +// Schema defines the schema for the resource. +func (r *projectRoleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Create custom project roles & save to Infisical. Only Machine Identity authentication is supported for this data source.", + Attributes: map[string]schema.Attribute{ + "slug": schema.StringAttribute{ + Description: "The slug for the new role", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "The name for the new role", + Required: true, + }, + "description": schema.StringAttribute{ + Description: "The description for the new role", + Optional: true, + }, + "project_slug": schema.StringAttribute{ + Description: "The slug of the project to create role", + Required: true, + }, + "id": schema.StringAttribute{ + Description: "The ID of the role", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "permissions": schema.ListNestedAttribute{ + Required: true, + Description: "The permissions assigned to the project role", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "action": schema.StringAttribute{ + Description: fmt.Sprintf("Describe what action an entity can take. Enum: %s", strings.Join(PERMISSION_ACTIONS, ",")), + Required: true, + }, + "subject": schema.StringAttribute{ + Description: fmt.Sprintf("Describe what action an entity can take. Enum: %s", strings.Join(PERMISSION_SUBJECTS, ",")), + Required: true, + }, + "conditions": schema.SingleNestedAttribute{ + Optional: true, + Description: "The conditions to scope permissions", + Attributes: map[string]schema.Attribute{ + "environment": schema.StringAttribute{ + Description: "The environment slug this permission should allow.", + Optional: true, + }, + "secret_path": schema.StringAttribute{ + Description: "The secret path this permission should be scoped to", + Optional: true, + }, + }, + }, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *projectRoleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*infisical.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +// Create creates the resource and sets the initial Terraform state. +func (r *projectRoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to create project role", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan projectRoleResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectRolePermissions := make([]infisicalclient.ProjectRolePermissionRequest, 0, len(plan.Permissions)) + for _, el := range plan.Permissions { + condition := make(map[string]any) + if el.Conditions != nil { + environment := el.Conditions.Environment.ValueString() + secretPath := el.Conditions.SecretPath.ValueString() + if environment != "" { + condition["environment"] = environment + } + if secretPath != "" { + condition["secretPath"] = map[string]string{"$glob": secretPath} + } + } else { + condition = nil + } + + projectRolePermissions = append(projectRolePermissions, infisicalclient.ProjectRolePermissionRequest{ + Action: el.Action.ValueString(), + Subject: el.Subject.ValueString(), + Conditions: condition, + }) + } + + newProjectRole, err := r.client.CreateProjectRole(infisical.CreateProjectRoleRequest{ + ProjectSlug: plan.ProjectSlug.ValueString(), + Slug: plan.Slug.ValueString(), + Name: plan.Name.ValueString(), + Description: plan.Description.ValueString(), + Permissions: projectRolePermissions, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error creating project role", + "Couldn't save project to Infiscial, unexpected error: "+err.Error(), + ) + return + } + + plan.ID = types.StringValue(newProjectRole.Role.ID) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Read refreshes the Terraform state with the latest data. +func (r *projectRoleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to read project role", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Get current state + var state projectRoleResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the latest data from the API + projectRole, err := r.client.GetProjectRoleBySlug(infisical.GetProjectRoleBySlugRequest{ + RoleSlug: state.Slug.ValueString(), + ProjectSlug: state.ProjectSlug.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error reading project role", + "Couldn't read project role from Infiscial, unexpected error: "+err.Error(), + ) + return + } + + state.Description = types.StringValue(projectRole.Role.Description) + state.ID = types.StringValue(projectRole.Role.ID) + state.Name = types.StringValue(projectRole.Role.Name) + + permissionPlan := make([]projectRoleResourcePermissions, 0, len(projectRole.Role.Permissions)) + for _, el := range projectRole.Role.Permissions { + action, isValid := el["action"].(string) + if el["action"] != nil && !isValid { + action, isValid = el["action"].([]any)[0].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project role", + "Couldn't read project role from Infiscial, invalid action field in permission", + ) + return + } + } + + subject, isValid := el["subject"].(string) + if el["subject"] != nil && !isValid { + subject, isValid = el["subject"].([]any)[0].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project role", + "Couldn't read project role from Infiscial, invalid subject field in permission", + ) + return + } + } + var secretPath, environment string + if el["conditions"] != nil { + conditions, isValid := el["conditions"].(map[string]any) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project role", + "Couldn't read project role from Infiscial, invalid conditions field in permission", + ) + return + } + + environment, isValid = conditions["environment"].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project role", + "Couldn't read project role from Infiscial, invalid environment field in permission", + ) + return + } + + // secret path parsing. + if val, isValid := conditions["secretPath"].(map[string]any); isValid { + secretPath, isValid = val["$glob"].(string) + if !isValid { + resp.Diagnostics.AddError( + "Error reading project role", + "Couldn't read project role from Infiscial, invalid secret path field in permission", + ) + return + } + } + } + + permissionPlan = append(permissionPlan, projectRoleResourcePermissions{ + Action: types.StringValue(action), + Subject: types.StringValue(subject), + Conditions: &projectRoleResourcePermissionCondition{ + Environment: types.StringValue(environment), + SecretPath: types.StringValue(secretPath), + }, + }) + } + + state.Permissions = permissionPlan + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *projectRoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to update project role", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan projectRoleResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var state projectRoleResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if state.ProjectSlug != plan.ProjectSlug { + resp.Diagnostics.AddError( + "Unable to update project role", + "Project slug cannot be updated", + ) + return + } + + projectRolePermissions := make([]infisicalclient.ProjectRolePermissionRequest, 0, len(plan.Permissions)) + for _, el := range plan.Permissions { + condition := make(map[string]any) + if el.Conditions != nil { + environment := el.Conditions.Environment.ValueString() + secretPath := el.Conditions.SecretPath.ValueString() + if environment != "" { + condition["environment"] = environment + } + if secretPath != "" { + condition["secretPath"] = map[string]string{"$glob": secretPath} + } + } else { + condition = nil + } + projectRolePermissions = append(projectRolePermissions, infisicalclient.ProjectRolePermissionRequest{ + Action: el.Action.ValueString(), + Subject: el.Subject.ValueString(), + Conditions: condition, + }) + } + + _, err := r.client.UpdateProjectRole(infisical.UpdateProjectRoleRequest{ + ProjectSlug: plan.ProjectSlug.ValueString(), + RoleId: plan.ID.ValueString(), + Slug: plan.Slug.ValueString(), + Name: plan.Name.ValueString(), + Description: plan.Description.ValueString(), + Permissions: projectRolePermissions, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error updating project role", + "Couldn't update project role from Infiscial, unexpected error: "+err.Error(), + ) + return + } + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *projectRoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to delete project role", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + var state projectRoleResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.DeleteProjectRole(infisical.DeleteProjectRoleRequest{ + ProjectSlug: state.ProjectSlug.ValueString(), + RoleId: state.ID.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project role", + "Couldn't delete project role from Infiscial, unexpected error: "+err.Error(), + ) + return + } + +} diff --git a/internal/provider/resource/project_user_resource.go b/internal/provider/resource/project_user_resource.go new file mode 100644 index 0000000..74148e5 --- /dev/null +++ b/internal/provider/resource/project_user_resource.go @@ -0,0 +1,578 @@ +package resource + +import ( + "context" + "fmt" + infisical "terraform-provider-infisical/internal/client" + "time" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &ProjectUserResource{} +) + +// NewProjectResource is a helper function to simplify the provider implementation. +func NewProjectUserResource() resource.Resource { + return &ProjectUserResource{} +} + +// ProjectUserResource is the resource implementation. +type ProjectUserResource struct { + client *infisical.Client +} + +// projectResourceSourceModel describes the data source data model. +type ProjectUserResourceModel struct { + ProjectID types.String `tfsdk:"project_id"` + Username types.String `tfsdk:"username"` + User types.Object `tfsdk:"user"` + Roles []ProjectUserRole `tfsdk:"roles"` + MembershipId types.String `tfsdk:"membership_id"` +} + +type ProjectUserPersonalDetails struct { + ID types.String `tfsdk:"id"` + Email types.String `tfsdk:"email"` + FirstName types.String `tfsdk:"first_name"` + LastName types.String `tfsdk:"last_name"` +} + +const TEMPORARY_MODE_RELATIVE = "relative" + +type ProjectUserRole struct { + ID types.String `tfsdk:"id"` + RoleSlug types.String `tfsdk:"role_slug"` + CustomRoleID types.String `tfsdk:"custom_role_id"` + IsTemporary types.Bool `tfsdk:"is_temporary"` + TemporaryMode types.String `tfsdk:"temporary_mode"` + TemporaryRange types.String `tfsdk:"temporary_range"` + TemporaryAccesStartTime types.String `tfsdk:"temporary_access_start_time"` + TemporaryAccessEndTime types.String `tfsdk:"temporary_access_end_time"` +} + +// Metadata returns the resource type name. +func (r *ProjectUserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_user" +} + +// Schema defines the schema for the resource. +func (r *ProjectUserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Create project users & save to Infisical. Only Machine Identity authentication is supported for this data source", + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "The id of the project", + Required: true, + }, + "username": schema.StringAttribute{ + Description: "The usename of the user. By default its the email", + Required: true, + }, + "membership_id": schema.StringAttribute{ + Description: "The membershipId of the project user", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "user": schema.SingleNestedAttribute{ + Computed: true, + Description: "The user details of the project user", + Attributes: map[string]schema.Attribute{ + "email": schema.StringAttribute{ + Description: "The email of the user", + Computed: true, + }, + "first_name": schema.StringAttribute{ + Description: "The first name of the user", + Computed: true, + }, + "last_name": schema.StringAttribute{ + Description: "The last name of the user", + Computed: true, + }, + "id": schema.StringAttribute{ + Description: "The id of the user", + Computed: true, + }, + }, + }, + "roles": schema.ListNestedAttribute{ + Required: true, + Description: "The roles assigned to the project user", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the project user role.", + Computed: true, + }, + "role_slug": schema.StringAttribute{ + Description: "The slug of the role", + Required: true, + }, + "custom_role_id": schema.StringAttribute{ + Description: "The id of the custom role slug", + Computed: true, + Optional: true, + }, + "is_temporary": schema.BoolAttribute{ + Description: "Flag to indicate the assigned role is temporary or not. When is_temporary is true fields temporary_mode, temporary_range and temporary_access_start_time is required.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "temporary_mode": schema.StringAttribute{ + Description: "Type of temporary access given. Types: relative. Default: relative", + Optional: true, + Computed: true, + }, + "temporary_range": schema.StringAttribute{ + Description: "TTL for the temporary time. Eg: 1m, 1h, 1d. Default: 1h", + Optional: true, + Computed: true, + }, + "temporary_access_start_time": schema.StringAttribute{ + Description: "ISO time for which temporary access should begin. The current time is used by default.", + Optional: true, + Computed: true, + }, + "temporary_access_end_time": schema.StringAttribute{ + Description: "ISO time for which temporary access will end. Computed based on temporary_range and temporary_access_start_time", + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *ProjectUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*infisical.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Source Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +// Create creates the resource and sets the initial Terraform state. +func (r *ProjectUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to create project user", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan ProjectUserResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var roles []infisical.UpdateProjectUserRequestRoles + var hasAtleastOnePermanentRole bool + for _, el := range plan.Roles { + isTemporary := el.IsTemporary.ValueBool() + temporaryMode := el.TemporaryMode.ValueString() + temporaryRange := el.TemporaryRange.ValueString() + temporaryAccesStartTime := time.Now().UTC() + + if !isTemporary { + hasAtleastOnePermanentRole = true + } + + if el.TemporaryAccesStartTime.ValueString() != "" { + var err error + temporaryAccesStartTime, err = time.Parse(time.RFC3339, el.TemporaryAccesStartTime.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing field TemporaryAccessStartTime", + fmt.Sprintf("Must provider valid ISO timestamp for field temporaryAccesStartTime %s, role %s", el.TemporaryAccesStartTime.ValueString(), el.RoleSlug.ValueString()), + ) + return + } + } + + // default values + if isTemporary && temporaryMode == "" { + temporaryMode = TEMPORARY_MODE_RELATIVE + } + if isTemporary && temporaryRange == "" { + temporaryRange = "1h" + } + + roles = append(roles, infisical.UpdateProjectUserRequestRoles{ + Role: el.RoleSlug.ValueString(), + IsTemporary: isTemporary, + TemporaryMode: temporaryMode, + TemporaryRange: temporaryRange, + TemporaryAccessStartTime: temporaryAccesStartTime, + }) + } + if !hasAtleastOnePermanentRole { + resp.Diagnostics.AddError("Error assigning role to user", "Must have atleast one permanent role") + return + } + + invitedUser, err := r.client.InviteUsersToProject(infisical.InviteUsersToProjectRequest{ + ProjectID: plan.ProjectID.ValueString(), + Usernames: []string{plan.Username.ValueString()}, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error inviting user", + "Couldn't create project user to Infiscial, unexpected error: "+err.Error(), + ) + return + } + + _, err = r.client.UpdateProjectUser(infisical.UpdateProjectUserRequest{ + ProjectID: plan.ProjectID.ValueString(), + MembershipID: invitedUser[0].ID, + Roles: roles, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error assigning roles to user", + "Couldn't update role , unexpected error: "+err.Error(), + ) + return + } + + projectUserDetails, err := r.client.GetProjectUserByUsername(infisical.GetProjectUserByUserNameRequest{ + ProjectID: plan.ProjectID.ValueString(), + Username: plan.Username.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error fetching user", + "Couldn't find user in project, unexpected error: "+err.Error(), + ) + return + } + + planRoles := make([]ProjectUserRole, 0, len(projectUserDetails.Membership.Roles)) + for _, el := range projectUserDetails.Membership.Roles { + val := ProjectUserRole{ + ID: types.StringValue(el.ID), + RoleSlug: types.StringValue(el.Role), + TemporaryAccessEndTime: types.StringValue(el.TemporaryAccessEndTime.Format(time.RFC3339)), + TemporaryRange: types.StringValue(el.TemporaryRange), + TemporaryMode: types.StringValue(el.TemporaryMode), + CustomRoleID: types.StringValue(el.CustomRoleId), + IsTemporary: types.BoolValue(el.IsTemporary), + TemporaryAccesStartTime: types.StringValue(el.TemporaryAccessStartTime.Format(time.RFC3339)), + } + + if el.CustomRoleId != "" { + val.RoleSlug = types.StringValue(el.CustomRoleSlug) + } + + if !el.IsTemporary { + val.TemporaryMode = types.StringNull() + val.TemporaryRange = types.StringNull() + val.TemporaryAccesStartTime = types.StringNull() + val.TemporaryAccessEndTime = types.StringNull() + } + planRoles = append(planRoles, val) + } + plan.Roles = planRoles + plan.MembershipId = types.StringValue(projectUserDetails.Membership.ID) + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + userPersonalDetails := ProjectUserPersonalDetails{ + Email: types.StringValue(projectUserDetails.Membership.User.Email), + FirstName: types.StringValue(projectUserDetails.Membership.User.FirstName), + LastName: types.StringValue(projectUserDetails.Membership.User.LastName), + ID: types.StringValue(projectUserDetails.Membership.User.ID), + } + diags = resp.State.SetAttribute(ctx, path.Root("user"), userPersonalDetails) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Read refreshes the Terraform state with the latest data. +func (r *ProjectUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to read project user", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Get current state + var state ProjectUserResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectUserDetails, err := r.client.GetProjectUserByUsername(infisical.GetProjectUserByUserNameRequest{ + ProjectID: state.ProjectID.ValueString(), + Username: state.Username.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error fetching user", + "Couldn't find user in project, unexpected error: "+err.Error(), + ) + return + } + + planRoles := make([]ProjectUserRole, 0, len(projectUserDetails.Membership.Roles)) + for _, el := range projectUserDetails.Membership.Roles { + val := ProjectUserRole{ + ID: types.StringValue(el.ID), + RoleSlug: types.StringValue(el.Role), + TemporaryAccessEndTime: types.StringValue(el.TemporaryAccessEndTime.Format(time.RFC3339)), + TemporaryRange: types.StringValue(el.TemporaryRange), + TemporaryMode: types.StringValue(el.TemporaryMode), + CustomRoleID: types.StringValue(el.CustomRoleId), + IsTemporary: types.BoolValue(el.IsTemporary), + TemporaryAccesStartTime: types.StringValue(el.TemporaryAccessStartTime.Format(time.RFC3339)), + } + if el.CustomRoleId != "" { + val.RoleSlug = types.StringValue(el.CustomRoleSlug) + } + if !el.IsTemporary { + val.TemporaryMode = types.StringNull() + val.TemporaryRange = types.StringNull() + val.TemporaryAccesStartTime = types.StringNull() + val.TemporaryAccessEndTime = types.StringNull() + } + planRoles = append(planRoles, val) + } + + state.Roles = planRoles + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + userPersonalDetails := ProjectUserPersonalDetails{ + Email: types.StringValue(projectUserDetails.Membership.User.Email), + FirstName: types.StringValue(projectUserDetails.Membership.User.FirstName), + LastName: types.StringValue(projectUserDetails.Membership.User.LastName), + ID: types.StringValue(projectUserDetails.Membership.User.ID), + } + diags = resp.State.SetAttribute(ctx, path.Root("user"), userPersonalDetails) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *ProjectUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to update project user", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan ProjectUserResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var state ProjectUserResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if state.Username != plan.Username { + resp.Diagnostics.AddError( + "Unable to update project user", + fmt.Sprintf("Cannot change username, previous username: %s, new username: %s", state.Username, plan.Username), + ) + return + } + + var roles []infisical.UpdateProjectUserRequestRoles + var hasAtleastOnePermanentRole bool + for _, el := range plan.Roles { + isTemporary := el.IsTemporary.ValueBool() + temporaryMode := el.TemporaryMode.ValueString() + temporaryRange := el.TemporaryRange.ValueString() + temporaryAccesStartTime := time.Now().UTC() + + if !isTemporary { + hasAtleastOnePermanentRole = true + } + + if el.TemporaryAccesStartTime.ValueString() != "" { + var err error + temporaryAccesStartTime, err = time.Parse(time.RFC3339, el.TemporaryAccesStartTime.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing field TemporaryAccessStartTime", + fmt.Sprintf("Must provider valid ISO timestamp for field temporaryAccesStartTime %s, role %s", el.TemporaryAccesStartTime.ValueString(), el.RoleSlug.ValueString()), + ) + return + } + } + + // default values + if isTemporary && temporaryMode == "" { + temporaryMode = TEMPORARY_MODE_RELATIVE + } + if isTemporary && temporaryRange == "" { + temporaryRange = "1h" + } + + roles = append(roles, infisical.UpdateProjectUserRequestRoles{ + Role: el.RoleSlug.ValueString(), + IsTemporary: isTemporary, + TemporaryMode: temporaryMode, + TemporaryRange: temporaryRange, + TemporaryAccessStartTime: temporaryAccesStartTime, + }) + } + + if !hasAtleastOnePermanentRole { + resp.Diagnostics.AddError("Error assigning role to user", "Must have atleast one permanent role") + return + } + + _, err := r.client.UpdateProjectUser(infisical.UpdateProjectUserRequest{ + ProjectID: plan.ProjectID.ValueString(), + MembershipID: plan.MembershipId.ValueString(), + Roles: roles, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error assigning roles to user", + "Couldn't update role , unexpected error: "+err.Error(), + ) + return + } + + projectUserDetails, err := r.client.GetProjectUserByUsername(infisical.GetProjectUserByUserNameRequest{ + ProjectID: plan.ProjectID.ValueString(), + Username: plan.Username.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error fetching user", + "Couldn't find user in project, unexpected error: "+err.Error(), + ) + return + } + + planRoles := make([]ProjectUserRole, 0, len(projectUserDetails.Membership.Roles)) + for _, el := range projectUserDetails.Membership.Roles { + val := ProjectUserRole{ + ID: types.StringValue(el.ID), + RoleSlug: types.StringValue(el.Role), + TemporaryAccessEndTime: types.StringValue(el.TemporaryAccessEndTime.Format(time.RFC3339)), + TemporaryRange: types.StringValue(el.TemporaryRange), + TemporaryMode: types.StringValue(el.TemporaryMode), + CustomRoleID: types.StringValue(el.CustomRoleId), + IsTemporary: types.BoolValue(el.IsTemporary), + TemporaryAccesStartTime: types.StringValue(el.TemporaryAccessStartTime.Format(time.RFC3339)), + } + if el.CustomRoleId != "" { + val.RoleSlug = types.StringValue(el.CustomRoleSlug) + } + if !el.IsTemporary { + val.TemporaryMode = types.StringNull() + val.TemporaryRange = types.StringNull() + val.TemporaryAccesStartTime = types.StringNull() + val.TemporaryAccessEndTime = types.StringNull() + } + planRoles = append(planRoles, val) + } + plan.Roles = planRoles + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + userPersonalDetails := ProjectUserPersonalDetails{ + Email: types.StringValue(projectUserDetails.Membership.User.Email), + FirstName: types.StringValue(projectUserDetails.Membership.User.FirstName), + LastName: types.StringValue(projectUserDetails.Membership.User.LastName), + ID: types.StringValue(projectUserDetails.Membership.User.ID), + } + diags = resp.State.SetAttribute(ctx, path.Root("user"), userPersonalDetails) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *ProjectUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + + if r.client.Config.AuthStrategy != infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { + resp.Diagnostics.AddError( + "Unable to delete project user", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + var state ProjectUserResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.DeleteProjectUser(infisical.DeleteProjectUserRequest{ + ProjectID: state.ProjectID.ValueString(), + Username: []string{state.Username.ValueString()}, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project user", + "Couldn't delete project user from Infiscial, unexpected error: "+err.Error(), + ) + return + } + +} diff --git a/infisical/provider/secret_resource.go b/internal/provider/resource/secret_resource.go similarity index 87% rename from infisical/provider/secret_resource.go rename to internal/provider/resource/secret_resource.go index f290ced..8d84ef8 100644 --- a/infisical/provider/secret_resource.go +++ b/internal/provider/resource/secret_resource.go @@ -1,10 +1,11 @@ -package provider +package resource import ( "context" "encoding/base64" "fmt" - infisical "terraform-provider-infisical/client" + infisical "terraform-provider-infisical/internal/client" + "terraform-provider-infisical/internal/crypto" "time" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -66,6 +67,7 @@ func (r *secretResource) Schema(_ context.Context, _ resource.SchemaRequest, res Description: "The value of the secret", Required: true, Computed: false, + Sensitive: true, }, "workspace_id": schema.StringAttribute{ Description: "The Infisical project ID (Required for Machine Identity auth, and service tokens with multiple scopes)", @@ -113,7 +115,7 @@ func (r *secretResource) Create(ctx context.Context, req resource.CreateRequest, if r.client.Config.AuthStrategy == infisical.AuthStrategy.SERVICE_TOKEN { - serviceTokenDetails, err := r.client.CallGetServiceTokenDetailsV2() + serviceTokenDetails, err := r.client.GetServiceTokenDetailsV2() if err != nil { resp.Diagnostics.AddError( "Error creating secret", @@ -141,7 +143,7 @@ func (r *secretResource) Create(ctx context.Context, req resource.CreateRequest, return } - plainTextWorkspaceKey, err := infisical.DecryptSymmetric([]byte(symmetricKeyFromServiceToken), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) + plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(symmetricKeyFromServiceToken), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) if err != nil { resp.Diagnostics.AddError( "Error creating secret", @@ -151,7 +153,7 @@ func (r *secretResource) Create(ctx context.Context, req resource.CreateRequest, } // encrypt key - encryptedKey, err := infisical.EncryptSymmetric([]byte(plan.Name.ValueString()), []byte(plainTextWorkspaceKey)) + encryptedKey, err := crypto.EncryptSymmetric([]byte(plan.Name.ValueString()), plainTextWorkspaceKey) if err != nil { resp.Diagnostics.AddError( "Error creating secret", @@ -161,7 +163,7 @@ func (r *secretResource) Create(ctx context.Context, req resource.CreateRequest, } // encrypt value - encryptedValue, err := infisical.EncryptSymmetric([]byte(plan.Value.ValueString()), []byte(plainTextWorkspaceKey)) + encryptedValue, err := crypto.EncryptSymmetric([]byte(plan.Value.ValueString()), plainTextWorkspaceKey) if err != nil { resp.Diagnostics.AddError( "Error creating secret", @@ -170,7 +172,7 @@ func (r *secretResource) Create(ctx context.Context, req resource.CreateRequest, return } - err = r.client.CallCreateSecretsV3(infisical.CreateSecretV3Request{ + err = r.client.CreateSecretsV3(infisical.CreateSecretV3Request{ Environment: plan.EnvSlug.ValueString(), SecretName: plan.Name.ValueString(), Type: "shared", @@ -197,7 +199,7 @@ func (r *secretResource) Create(ctx context.Context, req resource.CreateRequest, // Set state to fully populated data plan.WorkspaceId = types.StringValue(serviceTokenDetails.Workspace) } else if r.client.Config.AuthStrategy == infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { - err := r.client.CallCreateRawSecretsV3(infisical.CreateRawSecretV3Request{ + err := r.client.CreateRawSecretsV3(infisical.CreateRawSecretV3Request{ Environment: plan.EnvSlug.ValueString(), WorkspaceID: plan.WorkspaceId.ValueString(), Type: "shared", @@ -244,7 +246,7 @@ func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, res if r.client.Config.AuthStrategy == infisical.AuthStrategy.SERVICE_TOKEN { - serviceTokenDetails, err := r.client.CallGetServiceTokenDetailsV2() + serviceTokenDetails, err := r.client.GetServiceTokenDetailsV2() if err != nil { resp.Diagnostics.AddError( "Error creating secret", @@ -272,7 +274,7 @@ func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, res return } - plainTextWorkspaceKey, err := infisical.DecryptSymmetric([]byte(symmetricKeyFromServiceToken), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) + plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(symmetricKeyFromServiceToken), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) if err != nil { resp.Diagnostics.AddError( "Error creating secret", @@ -282,7 +284,7 @@ func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, res } // Get refreshed order value from HashiCups - response, err := r.client.CallGetSingleSecretByNameV3(infisical.GetSingleSecretByNameV3Request{ + response, err := r.client.GetSingleSecretByNameV3(infisical.GetSingleSecretByNameV3Request{ SecretName: state.Name.ValueString(), Type: "shared", WorkspaceId: state.WorkspaceId.ValueString(), @@ -326,7 +328,7 @@ func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, res return } - plainTextKey, err := infisical.DecryptSymmetric(plainTextWorkspaceKey, key_ciphertext, key_tag, key_iv) + plainTextKey, err := crypto.DecryptSymmetric(plainTextWorkspaceKey, key_ciphertext, key_tag, key_iv) if err != nil { resp.Diagnostics.AddError( "Error Reading Infisical secret", @@ -354,7 +356,7 @@ func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, res return } - value_ciphertext, _ := base64.StdEncoding.DecodeString(response.Secret.SecretValueCiphertext) + value_ciphertext, err := base64.StdEncoding.DecodeString(response.Secret.SecretValueCiphertext) if err != nil { resp.Diagnostics.AddError( "Error Reading Infisical secret", @@ -363,7 +365,7 @@ func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, res return } - plainTextValue, err := infisical.DecryptSymmetric(plainTextWorkspaceKey, value_ciphertext, value_tag, value_iv) + plainTextValue, err := crypto.DecryptSymmetric(plainTextWorkspaceKey, value_ciphertext, value_tag, value_iv) if err != nil { resp.Diagnostics.AddError( "Error Reading Infisical secret", @@ -377,7 +379,7 @@ func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, res } else if r.client.Config.AuthStrategy == infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { // Get refreshed order value from HashiCups - response, err := r.client.CallGetSingleRawSecretByNameV3(infisical.GetSingleSecretByNameV3Request{ + response, err := r.client.GetSingleRawSecretByNameV3(infisical.GetSingleSecretByNameV3Request{ SecretName: state.Name.ValueString(), Type: "shared", WorkspaceId: state.WorkspaceId.ValueString(), @@ -439,7 +441,7 @@ func (r *secretResource) Update(ctx context.Context, req resource.UpdateRequest, if r.client.Config.AuthStrategy == infisical.AuthStrategy.SERVICE_TOKEN { - serviceTokenDetails, err := r.client.CallGetServiceTokenDetailsV2() + serviceTokenDetails, err := r.client.GetServiceTokenDetailsV2() if err != nil { resp.Diagnostics.AddError( "Error updating secret", @@ -467,7 +469,7 @@ func (r *secretResource) Update(ctx context.Context, req resource.UpdateRequest, return } - plainTextWorkspaceKey, err := infisical.DecryptSymmetric([]byte(symmetricKeyFromServiceToken), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) + plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(symmetricKeyFromServiceToken), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) if err != nil { resp.Diagnostics.AddError( "Error updating secret", @@ -477,7 +479,7 @@ func (r *secretResource) Update(ctx context.Context, req resource.UpdateRequest, } // encrypt value - encryptedSecretValue, err := infisical.EncryptSymmetric([]byte(plan.Value.ValueString()), []byte(plainTextWorkspaceKey)) + encryptedSecretValue, err := crypto.EncryptSymmetric([]byte(plan.Value.ValueString()), plainTextWorkspaceKey) if err != nil { resp.Diagnostics.AddError( "Error updating secret", @@ -486,15 +488,7 @@ func (r *secretResource) Update(ctx context.Context, req resource.UpdateRequest, return } - if err != nil { - resp.Diagnostics.AddError( - "Error updating secret", - "Couldn't encrypt secret key, unexpected error: "+err.Error(), - ) - return - } - - err = r.client.CallUpdateSecretsV3(infisical.UpdateSecretByNameV3Request{ + err = r.client.UpdateSecretsV3(infisical.UpdateSecretByNameV3Request{ Environment: plan.EnvSlug.ValueString(), SecretName: plan.Name.ValueString(), Type: "shared", @@ -518,7 +512,7 @@ func (r *secretResource) Update(ctx context.Context, req resource.UpdateRequest, plan.WorkspaceId = types.StringValue(serviceTokenDetails.Workspace) } else if r.client.Config.AuthStrategy == infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { - err := r.client.CallUpdateRawSecretV3(infisical.UpdateRawSecretByNameV3Request{ + err := r.client.UpdateRawSecretV3(infisical.UpdateRawSecretByNameV3Request{ Environment: plan.EnvSlug.ValueString(), WorkspaceID: plan.WorkspaceId.ValueString(), Type: "shared", @@ -568,7 +562,7 @@ func (r *secretResource) Delete(ctx context.Context, req resource.DeleteRequest, if r.client.Config.AuthStrategy == infisical.AuthStrategy.SERVICE_TOKEN { // Delete existing order - err := r.client.CallDeleteSecretsV3(infisical.DeleteSecretV3Request{ + err := r.client.DeleteSecretsV3(infisical.DeleteSecretV3Request{ SecretName: state.Name.ValueString(), SecretPath: state.FolderPath.ValueString(), Environment: state.EnvSlug.ValueString(), @@ -584,7 +578,7 @@ func (r *secretResource) Delete(ctx context.Context, req resource.DeleteRequest, return } } else if r.client.Config.AuthStrategy == infisical.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { - err := r.client.CallDeleteRawSecretV3(infisical.DeleteRawSecretV3Request{ + err := r.client.DeleteRawSecretV3(infisical.DeleteRawSecretV3Request{ SecretName: state.Name.ValueString(), SecretPath: state.FolderPath.ValueString(), Environment: state.EnvSlug.ValueString(), diff --git a/main.go b/main.go index 9eecd84..8c43f4c 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "flag" "log" - "terraform-provider-infisical/infisical/provider" + "terraform-provider-infisical/internal/provider" "github.com/hashicorp/terraform-plugin-framework/providerserver" )