diff --git a/sysdig/internal/client/v2/model.go b/sysdig/internal/client/v2/model.go index b8112995..3d131f21 100644 --- a/sysdig/internal/client/v2/model.go +++ b/sysdig/internal/client/v2/model.go @@ -1201,3 +1201,80 @@ type ZoneScope struct { TargetType string `json:"targetType"` Rules string `json:"rules"` } + +// SSO OpenID Connect configuration - combines base SSO fields with OpenID-specific config +type SSOOpenID struct { + // Response-only identification fields + ID int `json:"id,omitempty"` + Version int `json:"version,omitempty"` + DateCreated string `json:"dateCreated,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + + // Base SSO fields (root level in API) + Product string `json:"product,omitempty"` + IsActive bool `json:"isActive"` + CreateUserOnLogin bool `json:"createUserOnLogin"` + IsSingleLogoutEnabled bool `json:"isSingleLogoutEnabled"` + IsGroupMappingEnabled bool `json:"isGroupMappingEnabled"` + GroupMappingAttributeName string `json:"groupMappingAttributeName,omitempty"` + IntegrationName string `json:"integrationName,omitempty"` + + // OpenID-specific config (nested in "config" in API) + Config *SSOOpenIDConfig `json:"config,omitempty"` +} + +// SSOOpenIDConfig contains OpenID-specific fields that go inside the "config" block +type SSOOpenIDConfig struct { + Type string `json:"type"` // "OPENID" + IssuerURL string `json:"issuerUrl"` + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret,omitempty"` + IsMetadataDiscoveryEnabled bool `json:"isMetadataDiscoveryEnabled"` + Metadata *OpenIDMetadata `json:"metadata,omitempty"` + GroupAttributeName string `json:"groupAttributeName,omitempty"` + IsAdditionalScopesCheckEnabled bool `json:"isAdditionalScopesCheckEnabled"` + AdditionalScopes []string `json:"additionalScopes,omitempty"` +} + +type OpenIDMetadata struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorizationEndpoint"` + TokenEndpoint string `json:"tokenEndpoint"` + JwksURI string `json:"jwksUri"` + TokenAuthMethod string `json:"tokenAuthMethod"` + EndSessionEndpoint string `json:"endSessionEndpoint,omitempty"` + UserInfoEndpoint string `json:"userInfoEndpoint,omitempty"` +} + +// SSO SAML configuration - combines base SSO fields with SAML-specific config +type SSOSaml struct { + // Response-only identification fields + ID int `json:"id,omitempty"` + Version int `json:"version,omitempty"` + DateCreated string `json:"dateCreated,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + + // Base SSO fields (root level in API) + Product string `json:"product,omitempty"` + IsActive bool `json:"isActive"` + CreateUserOnLogin bool `json:"createUserOnLogin"` + IsSingleLogoutEnabled bool `json:"isSingleLogoutEnabled"` + IsGroupMappingEnabled bool `json:"isGroupMappingEnabled"` + GroupMappingAttributeName string `json:"groupMappingAttributeName,omitempty"` + IntegrationName string `json:"integrationName,omitempty"` + + // SAML-specific config (nested in "config" in API) + Config *SSOSamlConfig `json:"config,omitempty"` +} + +// SSOSamlConfig contains SAML-specific fields that go inside the "config" block +type SSOSamlConfig struct { + Type string `json:"type"` // "SAML" + MetadataURL string `json:"metadataUrl,omitempty"` + MetadataXML string `json:"metadataXml,omitempty"` + EmailParameter string `json:"emailParameter"` + IsSignatureValidationEnabled *bool `json:"isSignatureValidationEnabled,omitempty"` + IsSignedAssertionEnabled *bool `json:"isSignedAssertionEnabled,omitempty"` + IsDestinationVerificationEnabled *bool `json:"isDestinationVerificationEnabled,omitempty"` + IsEncryptionSupportEnabled *bool `json:"isEncryptionSupportEnabled,omitempty"` +} diff --git a/sysdig/internal/client/v2/sso_openid.go b/sysdig/internal/client/v2/sso_openid.go new file mode 100644 index 00000000..5a25c25c --- /dev/null +++ b/sysdig/internal/client/v2/sso_openid.go @@ -0,0 +1,126 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +var ErrSSOOpenIDNotFound = errors.New("SSO OpenID configuration not found") + +const ( + createSSOOpenIDPath = "%s/platform/v1/sso-settings/" + getSSOOpenIDPath = "%s/platform/v1/sso-settings/%d" + updateSSOOpenIDPath = "%s/platform/v1/sso-settings/%d" + deleteSSOOpenIDPath = "%s/platform/v1/sso-settings/%d" +) + +type SSOOpenIDInterface interface { + Base + CreateSSOOpenID(ctx context.Context, sso *SSOOpenID) (*SSOOpenID, error) + GetSSOOpenID(ctx context.Context, id int) (*SSOOpenID, error) + UpdateSSOOpenID(ctx context.Context, id int, sso *SSOOpenID) (*SSOOpenID, error) + DeleteSSOOpenID(ctx context.Context, id int) error +} + +func (c *Client) CreateSSOOpenID(ctx context.Context, sso *SSOOpenID) (result *SSOOpenID, err error) { + payload, err := Marshal(sso) + if err != nil { + return nil, err + } + + response, err := c.requester.Request(ctx, http.MethodPost, c.createSSOOpenIDURL(), payload) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated { + return nil, c.ErrorFromResponse(response) + } + + return Unmarshal[*SSOOpenID](response.Body) +} + +func (c *Client) GetSSOOpenID(ctx context.Context, id int) (result *SSOOpenID, err error) { + response, err := c.requester.Request(ctx, http.MethodGet, c.getSSOOpenIDURL(id), nil) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode == http.StatusNotFound { + return nil, ErrSSOOpenIDNotFound + } + if response.StatusCode != http.StatusOK { + return nil, c.ErrorFromResponse(response) + } + + return Unmarshal[*SSOOpenID](response.Body) +} + +func (c *Client) UpdateSSOOpenID(ctx context.Context, id int, sso *SSOOpenID) (result *SSOOpenID, err error) { + payload, err := Marshal(sso) + if err != nil { + return nil, err + } + + response, err := c.requester.Request(ctx, http.MethodPut, c.updateSSOOpenIDURL(id), payload) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusOK { + return nil, c.ErrorFromResponse(response) + } + + return Unmarshal[*SSOOpenID](response.Body) +} + +func (c *Client) DeleteSSOOpenID(ctx context.Context, id int) (err error) { + response, err := c.requester.Request(ctx, http.MethodDelete, c.deleteSSOOpenIDURL(id), nil) + if err != nil { + return err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound { + return c.ErrorFromResponse(response) + } + + return nil +} + +func (c *Client) createSSOOpenIDURL() string { + return fmt.Sprintf(createSSOOpenIDPath, c.config.url) +} + +func (c *Client) getSSOOpenIDURL(id int) string { + return fmt.Sprintf(getSSOOpenIDPath, c.config.url, id) +} + +func (c *Client) updateSSOOpenIDURL(id int) string { + return fmt.Sprintf(updateSSOOpenIDPath, c.config.url, id) +} + +func (c *Client) deleteSSOOpenIDURL(id int) string { + return fmt.Sprintf(deleteSSOOpenIDPath, c.config.url, id) +} diff --git a/sysdig/internal/client/v2/sso_saml.go b/sysdig/internal/client/v2/sso_saml.go new file mode 100644 index 00000000..5ba8f59c --- /dev/null +++ b/sysdig/internal/client/v2/sso_saml.go @@ -0,0 +1,126 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +var ErrSSOSamlNotFound = errors.New("SSO SAML configuration not found") + +const ( + createSSOSamlPath = "%s/platform/v1/sso-settings/" + getSSOSamlPath = "%s/platform/v1/sso-settings/%d" + updateSSOSamlPath = "%s/platform/v1/sso-settings/%d" + deleteSSOSamlPath = "%s/platform/v1/sso-settings/%d" +) + +type SSOSamlInterface interface { + Base + CreateSSOSaml(ctx context.Context, sso *SSOSaml) (*SSOSaml, error) + GetSSOSaml(ctx context.Context, id int) (*SSOSaml, error) + UpdateSSOSaml(ctx context.Context, id int, sso *SSOSaml) (*SSOSaml, error) + DeleteSSOSaml(ctx context.Context, id int) error +} + +func (c *Client) CreateSSOSaml(ctx context.Context, sso *SSOSaml) (result *SSOSaml, err error) { + payload, err := Marshal(sso) + if err != nil { + return nil, err + } + + response, err := c.requester.Request(ctx, http.MethodPost, c.createSSOSamlURL(), payload) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusCreated { + return nil, c.ErrorFromResponse(response) + } + + return Unmarshal[*SSOSaml](response.Body) +} + +func (c *Client) GetSSOSaml(ctx context.Context, id int) (result *SSOSaml, err error) { + response, err := c.requester.Request(ctx, http.MethodGet, c.getSSOSamlURL(id), nil) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode == http.StatusNotFound { + return nil, ErrSSOSamlNotFound + } + if response.StatusCode != http.StatusOK { + return nil, c.ErrorFromResponse(response) + } + + return Unmarshal[*SSOSaml](response.Body) +} + +func (c *Client) UpdateSSOSaml(ctx context.Context, id int, sso *SSOSaml) (result *SSOSaml, err error) { + payload, err := Marshal(sso) + if err != nil { + return nil, err + } + + response, err := c.requester.Request(ctx, http.MethodPut, c.updateSSOSamlURL(id), payload) + if err != nil { + return nil, err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusOK { + return nil, c.ErrorFromResponse(response) + } + + return Unmarshal[*SSOSaml](response.Body) +} + +func (c *Client) DeleteSSOSaml(ctx context.Context, id int) (err error) { + response, err := c.requester.Request(ctx, http.MethodDelete, c.deleteSSOSamlURL(id), nil) + if err != nil { + return err + } + defer func() { + if dErr := response.Body.Close(); dErr != nil { + err = fmt.Errorf("unable to close response body: %w", dErr) + } + }() + + if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotFound { + return c.ErrorFromResponse(response) + } + + return nil +} + +func (c *Client) createSSOSamlURL() string { + return fmt.Sprintf(createSSOSamlPath, c.config.url) +} + +func (c *Client) getSSOSamlURL(id int) string { + return fmt.Sprintf(getSSOSamlPath, c.config.url, id) +} + +func (c *Client) updateSSOSamlURL(id int) string { + return fmt.Sprintf(updateSSOSamlPath, c.config.url, id) +} + +func (c *Client) deleteSSOSamlURL(id int) string { + return fmt.Sprintf(deleteSSOSamlPath, c.config.url, id) +} diff --git a/sysdig/internal/client/v2/sysdig.go b/sysdig/internal/client/v2/sysdig.go index 3316182a..09e19d6a 100644 --- a/sysdig/internal/client/v2/sysdig.go +++ b/sysdig/internal/client/v2/sysdig.go @@ -25,6 +25,8 @@ type SysdigCommon interface { GroupMappingInterface IPFilteringSettingsInterface IPFiltersInterface + SSOOpenIDInterface + SSOSamlInterface TeamServiceAccountInterface } diff --git a/sysdig/provider.go b/sysdig/provider.go index 694a47c9..c567eb73 100644 --- a/sysdig/provider.go +++ b/sysdig/provider.go @@ -121,6 +121,8 @@ func (p *SysdigProvider) Provider() *schema.Provider { "sysdig_group_mapping_config": resourceSysdigGroupMappingConfig(), "sysdig_ip_filter": resourceSysdigIPFilter(), "sysdig_ip_filtering_settings": resourceSysdigIPFilteringSettings(), + "sysdig_sso_openid": resourceSysdigSSOOpenID(), + "sysdig_sso_saml": resourceSysdigSSOSaml(), "sysdig_team_service_account": resourceSysdigTeamServiceAccount(), "sysdig_user": resourceSysdigUser(), diff --git a/sysdig/resource_sysdig_secure_cloud_auth_account_test.go b/sysdig/resource_sysdig_secure_cloud_auth_account_test.go index a20bbb2e..527c35c9 100644 --- a/sysdig/resource_sysdig_secure_cloud_auth_account_test.go +++ b/sysdig/resource_sysdig_secure_cloud_auth_account_test.go @@ -574,7 +574,6 @@ func TestAccAWSSecureCloudAuthAccountResponseActions(t *testing.T) { }, }, }) - } func TestAccAWSSecureCloudAccountThreatDetection(t *testing.T) { diff --git a/sysdig/resource_sysdig_sso_openid.go b/sysdig/resource_sysdig_sso_openid.go new file mode 100644 index 00000000..8eaaa74a --- /dev/null +++ b/sysdig/resource_sysdig_sso_openid.go @@ -0,0 +1,412 @@ +package sysdig + +import ( + "context" + "fmt" + "strconv" + "time" + + v2 "github.com/draios/terraform-provider-sysdig/sysdig/internal/client/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceSysdigSSOOpenID() *schema.Resource { + timeout := 5 * time.Minute + + return &schema.Resource{ + ReadContext: resourceSysdigSSOOpenIDRead, + CreateContext: resourceSysdigSSOOpenIDCreate, + UpdateContext: resourceSysdigSSOOpenIDUpdate, + DeleteContext: resourceSysdigSSOOpenIDDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(timeout), + Update: schema.DefaultTimeout(timeout), + Read: schema.DefaultTimeout(timeout), + Delete: schema.DefaultTimeout(timeout), + }, + CustomizeDiff: validateSSOOpenIDMetadata, + Schema: map[string]*schema.Schema{ + // Required fields + "issuer_url": { + Type: schema.TypeString, + Required: true, + Description: "The OpenID Connect issuer URL (e.g., https://accounts.google.com)", + }, + "client_id": { + Type: schema.TypeString, + Required: true, + Description: "The OAuth 2.0 client ID", + }, + "client_secret": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "The OAuth 2.0 client secret", + }, + + // Optional base SSO fields + "product": { + Type: schema.TypeString, + Optional: true, + Default: "secure", + ValidateFunc: validation.StringInSlice([]string{"monitor", "secure"}, false), + Description: "The Sysdig product (monitor or secure)", + }, + "is_active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether the SSO configuration is active", + }, + "create_user_on_login": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to create a new user upon first login", + }, + "is_single_logout_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether single logout is enabled", + }, + "is_group_mapping_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether group mapping is enabled", + }, + "group_mapping_attribute_name": { + Type: schema.TypeString, + Optional: true, + Default: "groups", + Description: "The attribute name for group mapping", + }, + "integration_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "A name to distinguish different SSO integrations (cannot be changed after creation)", + }, + + // OpenID specific optional fields + "is_metadata_discovery_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether to use automatic metadata discovery from the issuer URL", + }, + "is_additional_scopes_check_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether additional scopes check is enabled", + }, + "additional_scopes": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Additional OAuth scopes to request", + }, + + // Metadata block (required if is_metadata_discovery_enabled = false) + "metadata": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "issuer": { + Type: schema.TypeString, + Required: true, + Description: "The issuer identifier", + }, + "authorization_endpoint": { + Type: schema.TypeString, + Required: true, + Description: "The authorization endpoint URL", + }, + "token_endpoint": { + Type: schema.TypeString, + Required: true, + Description: "The token endpoint URL", + }, + "jwks_uri": { + Type: schema.TypeString, + Required: true, + Description: "The JWKS URI for token verification", + }, + "token_auth_method": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"CLIENT_SECRET_BASIC", "CLIENT_SECRET_POST"}, false), + Description: "The token authentication method (CLIENT_SECRET_BASIC or CLIENT_SECRET_POST)", + }, + "end_session_endpoint": { + Type: schema.TypeString, + Optional: true, + Description: "The end session endpoint URL for logout", + }, + "user_info_endpoint": { + Type: schema.TypeString, + Optional: true, + Description: "The user info endpoint URL", + }, + }, + }, + Description: "Manual metadata configuration (required when is_metadata_discovery_enabled is false)", + }, + + // Computed field + "version": { + Type: schema.TypeInt, + Computed: true, + Description: "The version of the SSO configuration (used for optimistic locking)", + }, + }, + } +} + +func validateSSOOpenIDMetadata(_ context.Context, diff *schema.ResourceDiff, _ any) error { + isMetadataDiscoveryEnabled := diff.Get("is_metadata_discovery_enabled").(bool) + metadata := diff.Get("metadata").([]any) + + if !isMetadataDiscoveryEnabled && len(metadata) == 0 { + return fmt.Errorf("metadata block is required when is_metadata_discovery_enabled is false") + } + + return nil +} + +func resourceSysdigSSOOpenIDRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + sso, err := client.GetSSOOpenID(ctx, id) + if err != nil { + if err == v2.ErrSSOOpenIDNotFound { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + return ssoOpenIDToResourceData(sso, d) +} + +func resourceSysdigSSOOpenIDCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + sso := ssoOpenIDFromResourceData(d) + + created, err := client.CreateSSOOpenID(ctx, sso) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.Itoa(created.ID)) + + return resourceSysdigSSOOpenIDRead(ctx, d, m) +} + +func resourceSysdigSSOOpenIDUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + sso := ssoOpenIDFromResourceData(d) + sso.ID = id + sso.Version = d.Get("version").(int) + + _, err = client.UpdateSSOOpenID(ctx, id, sso) + if err != nil { + return diag.FromErr(err) + } + + return resourceSysdigSSOOpenIDRead(ctx, d, m) +} + +func resourceSysdigSSOOpenIDDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + // API requires disabling SSO config before deletion + // We need to build the object from ResourceData to include client_secret + // (which is not returned by GET but is required for PUT) + if d.Get("is_active").(bool) { + sso := ssoOpenIDFromResourceData(d) + sso.ID = id + sso.Version = d.Get("version").(int) + sso.IsActive = false + + _, err = client.UpdateSSOOpenID(ctx, id, sso) + if err != nil { + return diag.Errorf("failed to disable SSO config before deletion: %s", err) + } + } + + err = client.DeleteSSOOpenID(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func ssoOpenIDFromResourceData(d *schema.ResourceData) *v2.SSOOpenID { + // Build the OpenID-specific config (nested in API "config" field) + config := &v2.SSOOpenIDConfig{ + Type: "OPENID", + IssuerURL: d.Get("issuer_url").(string), + ClientID: d.Get("client_id").(string), + ClientSecret: d.Get("client_secret").(string), + IsMetadataDiscoveryEnabled: d.Get("is_metadata_discovery_enabled").(bool), + IsAdditionalScopesCheckEnabled: d.Get("is_additional_scopes_check_enabled").(bool), + } + + // Handle additional scopes + if v, ok := d.GetOk("additional_scopes"); ok { + scopesInterface := v.([]any) + scopes := make([]string, len(scopesInterface)) + for i, s := range scopesInterface { + scopes[i] = s.(string) + } + config.AdditionalScopes = scopes + } + + // Handle metadata block + if v, ok := d.GetOk("metadata"); ok { + metadataList := v.([]any) + if len(metadataList) > 0 { + metadata := metadataList[0].(map[string]any) + config.Metadata = &v2.OpenIDMetadata{ + Issuer: metadata["issuer"].(string), + AuthorizationEndpoint: metadata["authorization_endpoint"].(string), + TokenEndpoint: metadata["token_endpoint"].(string), + JwksURI: metadata["jwks_uri"].(string), + TokenAuthMethod: metadata["token_auth_method"].(string), + EndSessionEndpoint: metadata["end_session_endpoint"].(string), + UserInfoEndpoint: metadata["user_info_endpoint"].(string), + } + } + } + + // Build the main SSO object with base fields at root level + sso := &v2.SSOOpenID{ + Product: d.Get("product").(string), + IsActive: d.Get("is_active").(bool), + CreateUserOnLogin: d.Get("create_user_on_login").(bool), + IsSingleLogoutEnabled: d.Get("is_single_logout_enabled").(bool), + IsGroupMappingEnabled: d.Get("is_group_mapping_enabled").(bool), + GroupMappingAttributeName: d.Get("group_mapping_attribute_name").(string), + IntegrationName: d.Get("integration_name").(string), + Config: config, + } + + return sso +} + +func ssoOpenIDToResourceData(sso *v2.SSOOpenID, d *schema.ResourceData) diag.Diagnostics { + var diags diag.Diagnostics + + // Set base SSO fields (root level in API) + if err := d.Set("product", sso.Product); err != nil { + return diag.FromErr(err) + } + if err := d.Set("is_active", sso.IsActive); err != nil { + return diag.FromErr(err) + } + if err := d.Set("create_user_on_login", sso.CreateUserOnLogin); err != nil { + return diag.FromErr(err) + } + if err := d.Set("is_single_logout_enabled", sso.IsSingleLogoutEnabled); err != nil { + return diag.FromErr(err) + } + if err := d.Set("is_group_mapping_enabled", sso.IsGroupMappingEnabled); err != nil { + return diag.FromErr(err) + } + if err := d.Set("group_mapping_attribute_name", sso.GroupMappingAttributeName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("integration_name", sso.IntegrationName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("version", sso.Version); err != nil { + return diag.FromErr(err) + } + + // Set OpenID-specific fields from nested config + if sso.Config != nil { + if err := d.Set("issuer_url", sso.Config.IssuerURL); err != nil { + return diag.FromErr(err) + } + if err := d.Set("client_id", sso.Config.ClientID); err != nil { + return diag.FromErr(err) + } + // Note: client_secret is not returned by the API, so we don't set it here + // to avoid diff issues with the sensitive value + + if err := d.Set("is_metadata_discovery_enabled", sso.Config.IsMetadataDiscoveryEnabled); err != nil { + return diag.FromErr(err) + } + if err := d.Set("is_additional_scopes_check_enabled", sso.Config.IsAdditionalScopesCheckEnabled); err != nil { + return diag.FromErr(err) + } + // Only set additional_scopes if API returns them (preserves user-configured values) + if sso.Config.AdditionalScopes != nil { + if err := d.Set("additional_scopes", sso.Config.AdditionalScopes); err != nil { + return diag.FromErr(err) + } + } + + // Handle metadata block + if sso.Config.Metadata != nil { + metadata := []map[string]any{ + { + "issuer": sso.Config.Metadata.Issuer, + "authorization_endpoint": sso.Config.Metadata.AuthorizationEndpoint, + "token_endpoint": sso.Config.Metadata.TokenEndpoint, + "jwks_uri": sso.Config.Metadata.JwksURI, + "token_auth_method": sso.Config.Metadata.TokenAuthMethod, + "end_session_endpoint": sso.Config.Metadata.EndSessionEndpoint, + "user_info_endpoint": sso.Config.Metadata.UserInfoEndpoint, + }, + } + if err := d.Set("metadata", metadata); err != nil { + return diag.FromErr(err) + } + } + } + + return diags +} diff --git a/sysdig/resource_sysdig_sso_openid_test.go b/sysdig/resource_sysdig_sso_openid_test.go new file mode 100644 index 00000000..f2c3ba01 --- /dev/null +++ b/sysdig/resource_sysdig_sso_openid_test.go @@ -0,0 +1,300 @@ +//go:build tf_acc_sysdig_monitor || tf_acc_sysdig_secure || tf_acc_onprem_monitor || tf_acc_onprem_secure + +package sysdig_test + +import ( + "fmt" + "os" + "testing" + + "github.com/draios/terraform-provider-sysdig/sysdig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestAccSSOOpenID_Basic(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoOpenIDBasicConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "issuer_url", + "https://accounts.google.com", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "client_id", + "test-client-id", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "integration_name", + integrationName, + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "is_active", + "true", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "is_metadata_discovery_enabled", + "true", + ), + resource.TestCheckResourceAttrSet( + "sysdig_sso_openid.test", + "version", + ), + ), + }, + { + ResourceName: "sysdig_sso_openid.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"client_secret"}, + }, + }, + }) +} + +func TestAccSSOOpenID_WithMetadata(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoOpenIDWithMetadataConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_metadata", + "is_metadata_discovery_enabled", + "false", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_metadata", + "metadata.0.issuer", + "https://idp.example.com", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_metadata", + "metadata.0.authorization_endpoint", + "https://idp.example.com/oauth2/authorize", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_metadata", + "metadata.0.token_endpoint", + "https://idp.example.com/oauth2/token", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_metadata", + "metadata.0.jwks_uri", + "https://idp.example.com/.well-known/jwks.json", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_metadata", + "metadata.0.token_auth_method", + "CLIENT_SECRET_BASIC", + ), + ), + }, + { + ResourceName: "sysdig_sso_openid.test_metadata", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"client_secret"}, + }, + }, + }) +} + +func TestAccSSOOpenID_Update(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoOpenIDBasicConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "integration_name", + integrationName, + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "is_group_mapping_enabled", + "false", + ), + ), + }, + { + Config: ssoOpenIDUpdatedConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "integration_name", + integrationName, // integration_name cannot be updated (ForceNew) + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "is_group_mapping_enabled", + "true", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test", + "group_mapping_attribute_name", + "custom_groups", + ), + ), + }, + }, + }) +} + +func TestAccSSOOpenID_WithAdditionalScopes(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoOpenIDWithAdditionalScopesConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_scopes", + "is_additional_scopes_check_enabled", + "true", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_scopes", + "additional_scopes.#", + "2", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_scopes", + "additional_scopes.0", + "groups", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_openid.test_scopes", + "additional_scopes.1", + "roles", + ), + ), + }, + }, + }) +} + +func ssoOpenIDBasicConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_openid" "test" { + issuer_url = "https://accounts.google.com" + client_id = "test-client-id" + client_secret = "test-client-secret" + integration_name = "%s" + is_active = true +} +`, integrationName) +} + +func ssoOpenIDUpdatedConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_openid" "test" { + issuer_url = "https://accounts.google.com" + client_id = "test-client-id" + client_secret = "test-client-secret" + integration_name = "%s" + is_active = true + is_group_mapping_enabled = true + group_mapping_attribute_name = "custom_groups" +} +`, integrationName) +} + +func ssoOpenIDWithMetadataConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_openid" "test_metadata" { + issuer_url = "https://idp.example.com" + client_id = "test-client-id" + client_secret = "test-client-secret" + integration_name = "%s" + is_metadata_discovery_enabled = false + + metadata { + issuer = "https://idp.example.com" + authorization_endpoint = "https://idp.example.com/oauth2/authorize" + token_endpoint = "https://idp.example.com/oauth2/token" + jwks_uri = "https://idp.example.com/.well-known/jwks.json" + token_auth_method = "CLIENT_SECRET_BASIC" + end_session_endpoint = "https://idp.example.com/oauth2/logout" + user_info_endpoint = "https://idp.example.com/userinfo" + } +} +`, integrationName) +} + +func ssoOpenIDWithAdditionalScopesConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_openid" "test_scopes" { + issuer_url = "https://accounts.google.com" + client_id = "test-client-id" + client_secret = "test-client-secret" + integration_name = "%s" + is_additional_scopes_check_enabled = true + additional_scopes = ["groups", "roles"] +} +`, integrationName) +} diff --git a/sysdig/resource_sysdig_sso_saml.go b/sysdig/resource_sysdig_sso_saml.go new file mode 100644 index 00000000..854bfa3d --- /dev/null +++ b/sysdig/resource_sysdig_sso_saml.go @@ -0,0 +1,332 @@ +package sysdig + +import ( + "context" + "strconv" + "time" + + v2 "github.com/draios/terraform-provider-sysdig/sysdig/internal/client/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceSysdigSSOSaml() *schema.Resource { + timeout := 5 * time.Minute + + return &schema.Resource{ + ReadContext: resourceSysdigSSOSamlRead, + CreateContext: resourceSysdigSSOSamlCreate, + UpdateContext: resourceSysdigSSOSamlUpdate, + DeleteContext: resourceSysdigSSOSamlDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(timeout), + Update: schema.DefaultTimeout(timeout), + Read: schema.DefaultTimeout(timeout), + Delete: schema.DefaultTimeout(timeout), + }, + Schema: map[string]*schema.Schema{ + // SAML metadata - mutually exclusive + "metadata_url": { + Type: schema.TypeString, + Optional: true, + Description: "The URL to fetch SAML metadata from the IdP", + ExactlyOneOf: []string{"metadata_url", "metadata_xml"}, + }, + "metadata_xml": { + Type: schema.TypeString, + Optional: true, + Description: "The raw SAML metadata XML from the IdP", + ExactlyOneOf: []string{"metadata_url", "metadata_xml"}, + }, + + // Required field + "email_parameter": { + Type: schema.TypeString, + Required: true, + Description: "The SAML attribute name that contains the user's email address", + }, + + // Optional base SSO fields (shared with OpenID) + "product": { + Type: schema.TypeString, + Optional: true, + Default: "secure", + ValidateFunc: validation.StringInSlice([]string{"monitor", "secure"}, false), + Description: "The Sysdig product (monitor or secure)", + }, + "is_active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether the SSO configuration is active", + }, + "create_user_on_login": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to create a new user upon first login", + }, + "is_single_logout_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether single logout is enabled", + }, + "is_group_mapping_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether group mapping is enabled", + }, + "group_mapping_attribute_name": { + Type: schema.TypeString, + Optional: true, + Default: "groups", + Description: "The SAML attribute name for group mapping", + }, + "integration_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "A name to distinguish different SSO integrations (cannot be changed after creation)", + }, + + // SAML specific optional fields (security) + "is_signature_validation_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether SAML response signature validation is enabled", + }, + "is_signed_assertion_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether signed SAML assertions are required", + }, + "is_destination_verification_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether destination verification is enabled", + }, + "is_encryption_support_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether encryption support is enabled", + }, + + // Computed field + "version": { + Type: schema.TypeInt, + Computed: true, + Description: "The version of the SSO configuration (used for optimistic locking)", + }, + }, + } +} + +func resourceSysdigSSOSamlRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + sso, err := client.GetSSOSaml(ctx, id) + if err != nil { + if err == v2.ErrSSOSamlNotFound { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + return ssoSamlToResourceData(sso, d) +} + +func resourceSysdigSSOSamlCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + sso := ssoSamlFromResourceData(d) + + created, err := client.CreateSSOSaml(ctx, sso) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.Itoa(created.ID)) + + return resourceSysdigSSOSamlRead(ctx, d, m) +} + +func resourceSysdigSSOSamlUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + sso := ssoSamlFromResourceData(d) + sso.ID = id + sso.Version = d.Get("version").(int) + + _, err = client.UpdateSSOSaml(ctx, id, sso) + if err != nil { + return diag.FromErr(err) + } + + return resourceSysdigSSOSamlRead(ctx, d, m) +} + +func resourceSysdigSSOSamlDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + client, err := m.(SysdigClients).sysdigCommonClientV2() + if err != nil { + return diag.FromErr(err) + } + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + // API requires disabling SSO config before deletion + if d.Get("is_active").(bool) { + sso := ssoSamlFromResourceData(d) + sso.ID = id + sso.Version = d.Get("version").(int) + sso.IsActive = false + + _, err = client.UpdateSSOSaml(ctx, id, sso) + if err != nil { + return diag.Errorf("failed to disable SSO config before deletion: %s", err) + } + } + + err = client.DeleteSSOSaml(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func ssoSamlFromResourceData(d *schema.ResourceData) *v2.SSOSaml { + // Build the SAML-specific config (nested in API "config" field) + config := &v2.SSOSamlConfig{ + Type: "SAML", + MetadataURL: d.Get("metadata_url").(string), + MetadataXML: d.Get("metadata_xml").(string), + EmailParameter: d.Get("email_parameter").(string), + } + + // Handle SAML security fields (using pointers to distinguish unset from false) + isSignatureValidationEnabled := d.Get("is_signature_validation_enabled").(bool) + config.IsSignatureValidationEnabled = &isSignatureValidationEnabled + + isSignedAssertionEnabled := d.Get("is_signed_assertion_enabled").(bool) + config.IsSignedAssertionEnabled = &isSignedAssertionEnabled + + isDestinationVerificationEnabled := d.Get("is_destination_verification_enabled").(bool) + config.IsDestinationVerificationEnabled = &isDestinationVerificationEnabled + + isEncryptionSupportEnabled := d.Get("is_encryption_support_enabled").(bool) + config.IsEncryptionSupportEnabled = &isEncryptionSupportEnabled + + // Build the main SSO object with base fields at root level + sso := &v2.SSOSaml{ + Product: d.Get("product").(string), + IsActive: d.Get("is_active").(bool), + CreateUserOnLogin: d.Get("create_user_on_login").(bool), + IsSingleLogoutEnabled: d.Get("is_single_logout_enabled").(bool), + IsGroupMappingEnabled: d.Get("is_group_mapping_enabled").(bool), + GroupMappingAttributeName: d.Get("group_mapping_attribute_name").(string), + IntegrationName: d.Get("integration_name").(string), + Config: config, + } + + return sso +} + +func ssoSamlToResourceData(sso *v2.SSOSaml, d *schema.ResourceData) diag.Diagnostics { + var diags diag.Diagnostics + + // Set base SSO fields (root level in API) + if err := d.Set("product", sso.Product); err != nil { + return diag.FromErr(err) + } + if err := d.Set("is_active", sso.IsActive); err != nil { + return diag.FromErr(err) + } + if err := d.Set("create_user_on_login", sso.CreateUserOnLogin); err != nil { + return diag.FromErr(err) + } + if err := d.Set("is_single_logout_enabled", sso.IsSingleLogoutEnabled); err != nil { + return diag.FromErr(err) + } + if err := d.Set("is_group_mapping_enabled", sso.IsGroupMappingEnabled); err != nil { + return diag.FromErr(err) + } + if err := d.Set("group_mapping_attribute_name", sso.GroupMappingAttributeName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("integration_name", sso.IntegrationName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("version", sso.Version); err != nil { + return diag.FromErr(err) + } + + // Set SAML-specific fields from nested config + if sso.Config != nil { + if err := d.Set("metadata_url", sso.Config.MetadataURL); err != nil { + return diag.FromErr(err) + } + if err := d.Set("metadata_xml", sso.Config.MetadataXML); err != nil { + return diag.FromErr(err) + } + if err := d.Set("email_parameter", sso.Config.EmailParameter); err != nil { + return diag.FromErr(err) + } + + // Handle SAML security fields + if sso.Config.IsSignatureValidationEnabled != nil { + if err := d.Set("is_signature_validation_enabled", *sso.Config.IsSignatureValidationEnabled); err != nil { + return diag.FromErr(err) + } + } + if sso.Config.IsSignedAssertionEnabled != nil { + if err := d.Set("is_signed_assertion_enabled", *sso.Config.IsSignedAssertionEnabled); err != nil { + return diag.FromErr(err) + } + } + if sso.Config.IsDestinationVerificationEnabled != nil { + if err := d.Set("is_destination_verification_enabled", *sso.Config.IsDestinationVerificationEnabled); err != nil { + return diag.FromErr(err) + } + } + if sso.Config.IsEncryptionSupportEnabled != nil { + if err := d.Set("is_encryption_support_enabled", *sso.Config.IsEncryptionSupportEnabled); err != nil { + return diag.FromErr(err) + } + } + } + + return diags +} diff --git a/sysdig/resource_sysdig_sso_saml_test.go b/sysdig/resource_sysdig_sso_saml_test.go new file mode 100644 index 00000000..289bb6e2 --- /dev/null +++ b/sysdig/resource_sysdig_sso_saml_test.go @@ -0,0 +1,278 @@ +//go:build tf_acc_sysdig_monitor || tf_acc_sysdig_secure || tf_acc_onprem_monitor || tf_acc_onprem_secure + +package sysdig_test + +import ( + "fmt" + "os" + "testing" + + "github.com/draios/terraform-provider-sysdig/sysdig" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestAccSSOSaml_WithMetadataURL(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoSamlWithMetadataURLConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "metadata_url", + "https://idp.example.com/metadata", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "email_parameter", + "email", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "integration_name", + integrationName, + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "is_active", + "true", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "is_signature_validation_enabled", + "true", + ), + resource.TestCheckResourceAttrSet( + "sysdig_sso_saml.test", + "version", + ), + ), + }, + { + ResourceName: "sysdig_sso_saml.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSSOSaml_WithMetadataXML(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoSamlWithMetadataXMLConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "sysdig_sso_saml.test_xml", + "metadata_xml", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test_xml", + "email_parameter", + "email", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test_xml", + "integration_name", + integrationName, + ), + ), + }, + { + ResourceName: "sysdig_sso_saml.test_xml", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSSOSaml_Update(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoSamlWithMetadataURLConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "integration_name", + integrationName, + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "is_group_mapping_enabled", + "false", + ), + ), + }, + { + Config: ssoSamlUpdatedConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "integration_name", + fmt.Sprintf("%s-updated", integrationName), + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "is_group_mapping_enabled", + "true", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test", + "group_mapping_attribute_name", + "custom_groups", + ), + ), + }, + }, + }) +} + +func TestAccSSOSaml_SecuritySettings(t *testing.T) { + integrationName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + monitor := os.Getenv("SYSDIG_MONITOR_API_TOKEN") + secure := os.Getenv("SYSDIG_SECURE_API_TOKEN") + if monitor == "" && secure == "" { + t.Fatal("SYSDIG_MONITOR_API_TOKEN or SYSDIG_SECURE_API_TOKEN must be set for acceptance tests") + } + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "sysdig": func() (*schema.Provider, error) { + return sysdig.Provider(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: ssoSamlWithSecuritySettingsConfig(integrationName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test_security", + "is_signature_validation_enabled", + "false", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test_security", + "is_signed_assertion_enabled", + "false", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test_security", + "is_destination_verification_enabled", + "false", + ), + resource.TestCheckResourceAttr( + "sysdig_sso_saml.test_security", + "is_encryption_support_enabled", + "true", + ), + ), + }, + }, + }) +} + +func ssoSamlWithMetadataURLConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_saml" "test" { + metadata_url = "https://idp.example.com/metadata" + email_parameter = "email" + integration_name = "%s" + is_active = true +} +`, integrationName) +} + +func ssoSamlWithMetadataXMLConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_saml" "test_xml" { + metadata_xml = <<-EOF + + + + + + +EOF + email_parameter = "email" + integration_name = "%s" + is_active = true +} +`, integrationName) +} + +func ssoSamlUpdatedConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_saml" "test" { + metadata_url = "https://idp.example.com/metadata" + email_parameter = "email" + integration_name = "%s-updated" + is_active = true + is_group_mapping_enabled = true + group_mapping_attribute_name = "custom_groups" +} +`, integrationName) +} + +func ssoSamlWithSecuritySettingsConfig(integrationName string) string { + return fmt.Sprintf(` +resource "sysdig_sso_saml" "test_security" { + metadata_url = "https://idp.example.com/metadata" + email_parameter = "email" + integration_name = "%s" + is_active = true + is_signature_validation_enabled = false + is_signed_assertion_enabled = false + is_destination_verification_enabled = false + is_encryption_support_enabled = true +} +`, integrationName) +} diff --git a/website/docs/r/sso_openid.md b/website/docs/r/sso_openid.md new file mode 100644 index 00000000..f62c63c7 --- /dev/null +++ b/website/docs/r/sso_openid.md @@ -0,0 +1,144 @@ +--- +subcategory: "Sysdig Platform" +layout: "sysdig" +page_title: "Sysdig: sysdig_sso_openid" +description: |- + Creates an SSO OpenID Connect configuration in Sysdig. +--- + +# Resource: sysdig_sso_openid + +Creates an SSO OpenID Connect configuration in Sysdig. + +-> **Note:** Sysdig Terraform Provider is under rapid development at this point. If you experience any issue or discrepancy while using it, please make sure you have the latest version. If the issue persists, or you have a Feature Request to support an additional set of resources, please open a [new issue](https://github.com/sysdiglabs/terraform-provider-sysdig/issues/new) in the GitHub repository. + +## Example Usage + +### Basic Configuration with Metadata Discovery + +```terraform +resource "sysdig_sso_openid" "google" { + issuer_url = "https://accounts.google.com" + client_id = "your-client-id.apps.googleusercontent.com" + client_secret = "your-client-secret" + integration_name = "Google SSO" + + is_active = true + create_user_on_login = true + is_metadata_discovery_enabled = true +} +``` + +### Configuration with Manual Metadata + +When using an identity provider that doesn't support metadata discovery, you can provide the metadata manually: + +```terraform +resource "sysdig_sso_openid" "custom_idp" { + issuer_url = "https://idp.example.com" + client_id = "your-client-id" + client_secret = "your-client-secret" + integration_name = "Custom IDP" + + is_active = true + is_metadata_discovery_enabled = false + + metadata { + issuer = "https://idp.example.com" + authorization_endpoint = "https://idp.example.com/oauth2/authorize" + token_endpoint = "https://idp.example.com/oauth2/token" + jwks_uri = "https://idp.example.com/.well-known/jwks.json" + token_auth_method = "CLIENT_SECRET_BASIC" + end_session_endpoint = "https://idp.example.com/oauth2/logout" + user_info_endpoint = "https://idp.example.com/userinfo" + } +} +``` + +### Configuration with Group Mapping and Additional Scopes + +```terraform +resource "sysdig_sso_openid" "okta" { + issuer_url = "https://your-org.okta.com" + client_id = "your-client-id" + client_secret = "your-client-secret" + integration_name = "Okta SSO" + + is_active = true + create_user_on_login = true + is_group_mapping_enabled = true + group_mapping_attribute_name = "groups" + is_single_logout_enabled = true + + is_additional_scopes_check_enabled = true + additional_scopes = ["groups", "profile", "email"] +} +``` + +## Argument Reference + +### Required Arguments + +* `issuer_url` - (Required) The OpenID Connect issuer URL (e.g., `https://accounts.google.com`). + +* `client_id` - (Required) The OAuth 2.0 client ID. + +* `client_secret` - (Required, Sensitive) The OAuth 2.0 client secret. + +### Optional Arguments + +* `product` - (Optional) The Sysdig product to configure SSO for. Valid values are `monitor` or `secure`. Default is `secure`. + +* `is_active` - (Optional) Whether the SSO configuration is active. Default is `true`. + +* `create_user_on_login` - (Optional) Whether to create a new user upon first login. Default is `false`. + +* `is_single_logout_enabled` - (Optional) Whether single logout (SLO) is enabled. Default is `false`. + +* `is_group_mapping_enabled` - (Optional) Whether group mapping is enabled. Default is `false`. + +* `group_mapping_attribute_name` - (Optional) The attribute name for group mapping in the ID token claims. Default is `groups`. + +* `integration_name` - (Optional) A name to distinguish different SSO integrations. Users can select this integration on the login page. + +* `is_metadata_discovery_enabled` - (Optional) Whether to use automatic metadata discovery from the issuer URL. Default is `true`. + +* `is_additional_scopes_check_enabled` - (Optional) Whether additional scopes check is enabled. Default is `false`. + +* `additional_scopes` - (Optional) A list of additional OAuth scopes to request. + +* `metadata` - (Optional) Manual metadata configuration. Required when `is_metadata_discovery_enabled` is `false`. See [Metadata](#metadata) below for details. + +### Metadata + +The `metadata` block supports the following arguments: + +* `issuer` - (Required) The issuer identifier. + +* `authorization_endpoint` - (Required) The authorization endpoint URL. + +* `token_endpoint` - (Required) The token endpoint URL. + +* `jwks_uri` - (Required) The JWKS URI for token verification. + +* `token_auth_method` - (Required) The token authentication method. Valid values are `CLIENT_SECRET_BASIC` or `CLIENT_SECRET_POST`. + +* `end_session_endpoint` - (Optional) The end session endpoint URL for logout. + +* `user_info_endpoint` - (Optional) The user info endpoint URL. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `version` - The version of the SSO configuration (used for optimistic locking). + +## Import + +Sysdig SSO OpenID configurations can be imported using the ID, e.g. + +``` +$ terraform import sysdig_sso_openid.example 12345 +``` + +~> **Note:** The `client_secret` attribute cannot be imported and must be set in the configuration after import. diff --git a/website/docs/r/sso_saml.md b/website/docs/r/sso_saml.md new file mode 100644 index 00000000..016f9b2e --- /dev/null +++ b/website/docs/r/sso_saml.md @@ -0,0 +1,115 @@ +--- +subcategory: "Sysdig Platform" +layout: "sysdig" +page_title: "Sysdig: sysdig_sso_saml" +description: |- + Creates a SAML SSO configuration in Sysdig. +--- + +# Resource: sysdig_sso_saml + +Creates a SAML Single Sign-On (SSO) configuration in Sysdig. + +-> **Note:** Sysdig Terraform Provider is under rapid development at this point. If you experience any issue or discrepancy while using it, please make sure you have the latest version. If the issue persists, or you have a Feature Request to support an additional set of resources, please open a [new issue](https://github.com/sysdiglabs/terraform-provider-sysdig/issues/new) in the GitHub repository. + +## Example Usage + +### Basic example with metadata URL + +```terraform +resource "sysdig_sso_saml" "example" { + metadata_url = "https://idp.example.com/app/sysdig/sso/saml/metadata" + email_parameter = "email" + integration_name = "Corporate SAML SSO" + is_active = true +} +``` + +### Example with inline metadata XML + +```terraform +resource "sysdig_sso_saml" "example_xml" { + metadata_xml = <<-EOF + + + + + + +EOF + + email_parameter = "email" + integration_name = "Corporate SAML SSO" + is_active = true +} +``` + +### Example with group mapping enabled + +```terraform +resource "sysdig_sso_saml" "example_groups" { + metadata_url = "https://idp.example.com/app/sysdig/sso/saml/metadata" + email_parameter = "email" + integration_name = "Corporate SAML SSO" + is_active = true + is_group_mapping_enabled = true + group_mapping_attribute_name = "groups" +} +``` + +### Example with custom security settings + +```terraform +resource "sysdig_sso_saml" "example_security" { + metadata_url = "https://idp.example.com/app/sysdig/sso/saml/metadata" + email_parameter = "email" + integration_name = "Corporate SAML SSO" + is_active = true + is_signature_validation_enabled = true + is_signed_assertion_enabled = true + is_destination_verification_enabled = true + is_encryption_support_enabled = false +} +``` + +## Argument Reference + +### Required + +* `email_parameter` - (Required) The SAML attribute name that contains the user's email address. + +### Metadata (exactly one required) + +* `metadata_url` - (Optional) The URL to fetch SAML metadata from the Identity Provider. Mutually exclusive with `metadata_xml`. +* `metadata_xml` - (Optional) The raw SAML metadata XML from the Identity Provider. Mutually exclusive with `metadata_url`. + +### Optional + +* `product` - (Optional) The Sysdig product to configure SSO for. Valid values are `monitor` or `secure`. Default: `secure`. +* `is_active` - (Optional) Whether the SSO configuration is active. Default: `true`. +* `create_user_on_login` - (Optional) Whether to automatically create a new user upon first login via SSO. Default: `false`. +* `is_single_logout_enabled` - (Optional) Whether SAML Single Logout (SLO) is enabled. Default: `false`. +* `is_group_mapping_enabled` - (Optional) Whether group mapping from SAML attributes is enabled. Default: `false`. +* `group_mapping_attribute_name` - (Optional) The SAML attribute name that contains group membership information. Default: `groups`. +* `integration_name` - (Optional) A name to distinguish different SSO integrations. Users can select this integration on the login page. + +### Security Settings (Optional) + +* `is_signature_validation_enabled` - (Optional) Whether SAML response signature validation is enabled. Default: `true`. +* `is_signed_assertion_enabled` - (Optional) Whether signed SAML assertions are required. Default: `true`. +* `is_destination_verification_enabled` - (Optional) Whether destination verification in SAML responses is enabled. Default: `true`. +* `is_encryption_support_enabled` - (Optional) Whether SAML encryption support is enabled. Default: `false`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `version` - The version of the SSO configuration, used for optimistic locking during updates. + +## Import + +SAML SSO configurations can be imported using the SSO configuration ID: + +``` +$ terraform import sysdig_sso_saml.example 12345 +```