diff --git a/codefresh/cfclient/idp.go b/codefresh/cfclient/idp.go index 928a034..aa213cd 100644 --- a/codefresh/cfclient/idp.go +++ b/codefresh/cfclient/idp.go @@ -3,35 +3,200 @@ package cfclient import ( "errors" "fmt" + "log" + "net/url" ) type IDP struct { - Access_token string `json:"access_token,omitempty"` - Accounts []string `json:"accounts,omitempty"` - ApiHost string `json:"apiHost,omitempty"` - ApiPathPrefix string `json:"apiPathPrefix,omitempty"` - ApiURL string `json:"apiURL,omitempty"` - AppId string `json:"appId,omitempty"` - AuthURL string `json:"authURL,omitempty"` - ClientHost string `json:"clientHost,omitempty"` - ClientId string `json:"clientId,omitempty"` - ClientName string `json:"clientName,omitempty"` - ClientSecret string `json:"clientSecret,omitempty"` - ClientType string `json:"clientType,omitempty"` - CookieIv string `json:"cookieIv,omitempty"` - CookieKey string `json:"cookieKey,omitempty"` - DisplayName string `json:"displayName,omitempty"` - ID string `json:"_id,omitempty"` - IDPLoginUrl string `json:"IDPLoginUrl,omitempty"` - LoginUrl string `json:"loginUrl,omitempty"` - RedirectUiUrl string `json:"redirectUiUrl,omitempty"` - RedirectUrl string `json:"redirectUrl,omitempty"` - RefreshTokenURL string `json:"refreshTokenURL,omitempty"` - Scopes []string `json:"scopes,omitempty"` - Tenant string `json:"tenant,omitempty"` - TokenSecret string `json:"tokenSecret,omitempty"` - TokenURL string `json:"tokenURL,omitempty"` - UserProfileURL string `json:"userProfileURL,omitempty"` + ID string `json:"_id,omitempty"` + Access_token string `json:"access_token,omitempty"` + Accounts []string `json:"accounts,omitempty"` + ClientName string `json:"clientName,omitempty"` // IDP name + ClientType string `json:"clientType,omitempty"` // IDP type + DisplayName string `json:"displayName,omitempty"` + LoginUrl string `json:"loginUrl,omitempty"` // Login url in Codefresh + RedirectUiUrl string `json:"redirectUiUrl,omitempty"` // Redicrect url Codefresh UI + RedirectUrl string `json:"redirectUrl,omitempty"` + ClientId string `json:"clientId,omitempty"` // All providers (base) + ClientSecret string `json:"clientSecret,omitempty"` // All providers (base) + ApiHost string `json:"apiHost,omitempty"` // GitHub + ApiPathPrefix string `json:"apiPathPrefix,omitempty"` // Github + // Bitbucket, Gitlab + ApiURL string `json:"apiURL,omitempty"` + // Azure, Okta, onelogin,saml + AppId string `json:"appId,omitempty"` + // Github, Gitlab + AuthURL string `json:"authURL,omitempty"` + // saml, okta, onelogin, auth0, azure, google, google-cloud-sr + ClientHost string `json:"clientHost,omitempty"` + // Azure + CookieIv string `json:"cookieIv,omitempty"` + // Azure + CookieKey string `json:"cookieKey,omitempty"` + // Azure + IDPLoginUrl string `json:"IDPLoginUrl,omitempty"` + // Bitbucket + RefreshTokenURL string `json:"refreshTokenURL,omitempty"` + // Multiple - computed + Scopes []string `json:"scopes,omitempty"` + // Azure + Tenant string `json:"tenant,omitempty"` + TokenSecret string `json:"tokenSecret,omitempty"` + // Okta, Bitbucket, GitHub, Keycloak + TokenURL string `json:"tokenURL,omitempty"` + // Github, Gitlab + UserProfileURL string `json:"userProfileURL,omitempty"` + // Okta + SyncMirrorAccounts []string `json:"syncMirrorAccounts,omitempty"` + // Google, Ldap + AllowedGroupsForSync string `json:"allowedGroupsForSync,omitempty"` + // Google + Subject string `json:"subject,omitempty"` + // Google + KeyFile string `json:"keyfile,omitempty"` + // Google + SyncField string `json:"syncField,omitempty"` + // Azure + AutoGroupSync bool `json:"autoGroupSync,omitempty"` + // Google,Okta,saml + ActivateUserAfterSync bool `json:"activateUserAfterSync,omitempty"` + // Azure + SyncInterval string `json:"syncInterval,omitempty"` + // Onelogin + ApiClientId string `json:"apiClientId,omitempty"` + // Onelogin + ApiClientSecret string `json:"apiClientSecret,omitempty"` + // Keycloak + Host string `json:"host,omitempty"` + // keycloak + Realm string `json:"realm,omitempty"` + // SAML + EntryPoint string `json:"entryPoint,omitempty"` + // SAML + ApplicationCert string `json:"cert,omitempty"` + // SAML + SamlProvider string `json:"provider,omitempty"` + // ldap + Password string `json:"password,omitempty"` + Url string `json:"url,omitempty"` + DistinguishedName string `json:"distinguishedName,omitempty"` + SearchBase string `json:"searchBase,omitempty"` + SearchFilter string `json:"searchFilter,omitempty"` + SearchBaseForSync string `json:"searchBaseForSync,omitempty"` + Certificate string `json:"certificate,omitempty"` +} + +// Return the appropriate API endpoint for platform and account scoped IDPs +func getAPIEndpoint(isGlobal bool) string { + // If IDP is platform scoped + if isGlobal { + return "/admin/idp" + } else { + return "/idp/account" + } +} + +// Currently on create the API sometimes (like when creating saml idps) returns a different structure for accounts than on read making the client crash on decode +// For now we are disabling response decode and in the resource will instead call the read function again +func (client *Client) CreateIDP(idp *IDP, isGlobal bool) (id string, err error) { + + body, err := EncodeToJSON(idp) + + if err != nil { + return "", err + } + opts := RequestOptions{ + Path: getAPIEndpoint(isGlobal), + Method: "POST", + Body: body, + } + + resp, err := client.RequestAPI(&opts) + + if err != nil { + log.Printf("[DEBUG] Call to API for IDP creation failed with Error = %v for Body %v", err, body) + return "", err + } + + var respIDP map[string]interface{} + err = DecodeResponseInto(resp, &respIDP) + + if err != nil { + return "", nil + } + + return respIDP["id"].(string), nil +} + +// Currently on update the API returns a different structure for accounts than on read making the client crash on decode +// For now we are disabling response decode and in the resource will instead call the read function again +func (client *Client) UpdateIDP(idp *IDP, isGlobal bool) error { + + body, err := EncodeToJSON(idp) + + if err != nil { + return err + } + opts := RequestOptions{ + Path: getAPIEndpoint(isGlobal), + Method: "PUT", + Body: body, + } + + _, err = client.RequestAPI(&opts) + + if err != nil { + log.Printf("[DEBUG] Call to API for IDP update failed with Error = %v for Body %v", err, body) + return err + } + + // var respIDP IDP + // err = DecodeResponseInto(resp, &respIDP) + // if err != nil { + // return nil, err + // } + + return nil +} + +func (client *Client) DeleteIDP(id string) error { + baseUrl := getAPIEndpoint(true) + fullPath := fmt.Sprintf("%s/%s", baseUrl, url.PathEscape(id)) + opts := RequestOptions{ + Path: fullPath, + Method: "DELETE", + } + + _, err := client.RequestAPI(&opts) + + if err != nil { + return err + } + + return nil +} + +func (client *Client) DeleteIDPAccount(id string) error { + + body, err := EncodeToJSON(map[string]interface{}{"id": id}) + + if err != nil { + return err + } + + opts := RequestOptions{ + Path: getAPIEndpoint(false), + Method: "DELETE", + Body: body, + } + + _, err = client.RequestAPI(&opts) + + if err != nil { + return err + } + + return nil } // get all idps @@ -115,6 +280,22 @@ func (client *Client) GetAccountIDPs() (*[]IDP, error) { return &idps, nil } +func (client *Client) GetAccountIdpByID(idpID string) (*IDP, error) { + + idpList, err := client.GetAccountIDPs() + if err != nil { + return nil, err + } + + for _, idp := range *idpList { + if idp.ID == idpID { + return &idp, nil + } + } + + return nil, errors.New(fmt.Sprintf("[ERROR] IDP with ID %s isn't found.", idpID)) +} + // add account to idp func (client *Client) AddAccountToIDP(accountId, idpId string) error { diff --git a/codefresh/data_account_idp.go b/codefresh/data_account_idp.go new file mode 100644 index 0000000..21bfe15 --- /dev/null +++ b/codefresh/data_account_idp.go @@ -0,0 +1,101 @@ +package codefresh + +import ( + "fmt" + + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceAccountIdp() *schema.Resource { + return &schema.Resource{ + Description: "This data source retrieves an account level identity provider", + Read: dataSourceAccountIdpRead, + Schema: AccountIdpSchema(), + } +} + +// IdpSchema - +func AccountIdpSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "_id": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"_id", "client_name"}, + }, + "client_name": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"_id", "client_name"}, + }, + "display_name": { + Type: schema.TypeString, + Computed: true, + }, + "client_type": { + Type: schema.TypeString, + Computed: true, + }, + "redirect_url": { + Description: "API Callback url for the identity provider", + Type: schema.TypeString, + Computed: true, + }, + "redirect_ui_url": { + Description: "UI Callback url for the identity provider", + Type: schema.TypeString, + Computed: true, + }, + "login_url": { + Description: "Login url using the IDP to Codefresh", + Type: schema.TypeString, + Computed: true, + }, + } +} + +func dataSourceAccountIdpRead(d *schema.ResourceData, meta interface{}) error { + + client := meta.(*cfclient.Client) + + idps, err := client.GetAccountIDPs() + if err != nil { + return err + } + + _id, _idOk := d.GetOk("_id") + clientName, clientNameOk := d.GetOk("client_name") + + for _, idp := range *idps { + if clientNameOk && clientName.(string) != idp.ClientName { + continue + } + if _idOk && _id.(string) != idp.ID { + continue + } + + err = mapDataAccountIdpToResource(idp, d) + if err != nil { + return err + } + } + + if d.Id() == "" { + return fmt.Errorf("[EROOR] Idp wasn't found") + } + + return nil +} + +func mapDataAccountIdpToResource(cfClientIDP cfclient.IDP, d *schema.ResourceData) error { + + d.SetId(cfClientIDP.ID) + d.Set("client_name", cfClientIDP.ClientName) + d.Set("client_type", cfClientIDP.ClientType) + d.Set("display_name", cfClientIDP.DisplayName) + d.Set("redirect_url", cfClientIDP.RedirectUrl) + d.Set("redirect_ui_url", cfClientIDP.RedirectUiUrl) + d.Set("login_url", cfClientIDP.LoginUrl) + + return nil +} diff --git a/codefresh/internal/idp/doc.go b/codefresh/internal/idp/doc.go new file mode 100644 index 0000000..fe3c330 --- /dev/null +++ b/codefresh/internal/idp/doc.go @@ -0,0 +1,2 @@ +// Package idp is shared by idp-related resources. +package idp diff --git a/codefresh/internal/idp/schema.go b/codefresh/internal/idp/schema.go new file mode 100644 index 0000000..5851b98 --- /dev/null +++ b/codefresh/internal/idp/schema.go @@ -0,0 +1,504 @@ +package idp + +import ( + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +var ( + SupportedIdps = []string{GitHub, GitLab, Okta, Google, Auth0, Azure, OneLogin, Keycloak, SAML, LDAP} + IdpSchema = map[string]*schema.Schema{ + "display_name": { + Description: "The display name for the IDP.", + Type: schema.TypeString, + Required: true, + }, + "name": { + Description: "Name of the IDP, will be generated if not set", + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "client_type": { + Description: "Type of the IDP. Derived from idp specific config object (github, gitlab etc)", + Type: schema.TypeString, + Computed: true, + ForceNew: true, + }, + "redirect_url": { + Description: "API Callback url for the identity provider", + Type: schema.TypeString, + Computed: true, + }, + "redirect_ui_url": { + Description: "UI Callback url for the identity provider", + Type: schema.TypeString, + Computed: true, + }, + "login_url": { + Description: "Login url using the IDP to Codefresh", + Type: schema.TypeString, + Computed: true, + }, + "github": { + Description: "Settings for GitHub IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Description: "Client ID from Github", + Required: true, + }, + "client_secret": { + Type: schema.TypeString, + Description: "Client secret from GitHub", + Required: true, + Sensitive: true, + }, + "authentication_url": { + Type: schema.TypeString, + Description: "Authentication url, Defaults to https://github.com/login/oauth/authorize", + Optional: true, + Default: "https://github.com/login/oauth/authorize", + }, + "token_url": { + Type: schema.TypeString, + Description: "GitHub token endpoint url, Defaults to https://github.com/login/oauth/access_token", + Optional: true, + Default: "https://github.com/login/oauth/access_token", + }, + "user_profile_url": { + Type: schema.TypeString, + Description: "GitHub user profile url, Defaults to https://api.github.com/user", + Optional: true, + Default: "https://api.github.com/user", + }, + "api_host": { + Type: schema.TypeString, + Description: "GitHub API host, Defaults to api.github.com", + Optional: true, + Default: "api.github.com", + }, + "api_path_prefix": { + Type: schema.TypeString, + Description: "GitHub API url path prefix, defaults to /", + Optional: true, + Default: "/", + }, + }, + }, + }, + "gitlab": { + Description: "Settings for GitLab IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Description: "Client ID from Gitlab", + Required: true, + }, + "client_secret": { + Type: schema.TypeString, + Description: "Client secret from Gitlab", + Required: true, + Sensitive: true, + }, + "authentication_url": { + Type: schema.TypeString, + Description: "Authentication url, Defaults to https://gitlab.com", + Optional: true, + Default: "https://gitlab.com", + }, + "user_profile_url": { + Type: schema.TypeString, + Description: "User profile url, Defaults to https://gitlab.com/api/v4/user", + Optional: true, + Default: "https://gitlab.com/api/v4/user", + }, + "api_url": { + Type: schema.TypeString, + Description: "Base url for Gitlab API, Defaults to https://gitlab.com/api/v4/", + Optional: true, + Default: "https://gitlab.com/api/v4/", + }, + }, + }, + }, + "okta": { + Description: "Settings for Okta IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Description: "Client ID in Okta, must be unique across all identity providers in Codefresh", + Required: true, + }, + "client_secret": { + Type: schema.TypeString, + Description: "Client secret in Okta", + Required: true, + Sensitive: true, + }, + "client_host": { + Type: schema.TypeString, + Description: "The OKTA organization URL, for example, https://.okta.com", + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^(https?:\/\/)(\S+)(\.okta(preview|-emea)?\.com$)`), "must be a valid okta url"), + Required: true, + }, + "app_id": { + Type: schema.TypeString, + Description: "The Codefresh application ID in your OKTA organization", + Optional: true, + }, + "sync_mirror_accounts": { + Type: schema.TypeList, + Description: "The names of the additional Codefresh accounts to be synced from Okta", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "access_token": { + Type: schema.TypeString, + Description: "The Okta API token generated in Okta, used to sync groups and their users from Okta to Codefresh", + Optional: true, + }, + }, + }, + }, + "google": { + Description: "Settings for Google IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Description: "Client ID in Google, must be unique across all identity providers in Codefresh", + Required: true, + }, + "client_secret": { + Type: schema.TypeString, + Description: "Client secret in Google", + Required: true, + Sensitive: true, + }, + "admin_email": { + Type: schema.TypeString, + Description: "Email of a user with admin permissions on google, relevant only for synchronization", + Optional: true, + }, + "json_keyfile": { + Type: schema.TypeString, + Description: "JSON keyfile for google service account used for synchronization", + Optional: true, + }, + "allowed_groups_for_sync": { + Type: schema.TypeString, + Description: "Comma separated list of groups to sync", + Optional: true, + }, + "sync_field": { + Type: schema.TypeString, + Description: "Relevant for custom schema-based synchronization only. See Codefresh documentation", + Optional: true, + }, + }, + }, + }, + "auth0": { + Description: "Settings for Auth0 IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Description: "Client ID from Auth0", + Required: true, + }, + "client_secret": { + Type: schema.TypeString, + Description: "Client secret from Auth0", + Required: true, + Sensitive: true, + }, + "domain": { + Type: schema.TypeString, + Description: "The domain of the Auth0 application", + Required: true, + }, + }, + }, + }, + "azure": { + Description: "Settings for Azure IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_secret": { + Type: schema.TypeString, + Description: "Client secret from Azure", + Required: true, + Sensitive: true, + }, + "app_id": { + Type: schema.TypeString, + Description: "The Application ID from your Enterprise Application Properties in Azure AD", + Required: true, + }, + "tenant": { + Type: schema.TypeString, + Description: "Azure tenant", + Optional: true, + }, + "object_id": { + Type: schema.TypeString, + Description: "The Object ID from your Enterprise Application Properties in Azure AD", + Optional: true, + }, + "autosync_teams_and_users": { + Type: schema.TypeBool, + Description: "Set to true to sync user accounts in Azure AD to your Codefresh account", + Optional: true, + Default: false, + }, + "sync_interval": { + Type: schema.TypeInt, + Description: "Sync interval in hours for syncing user accounts in Azure AD to your Codefresh account. If not set the sync inteval will be 12 hours", + Optional: true, + }, + }, + }, + }, + "onelogin": { + Description: "Settings for onelogin IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Description: "Client ID from Onelogin", + Required: true, + }, + "client_secret": { + Type: schema.TypeString, + Description: "Client secret from Onelogin", + Required: true, + Sensitive: true, + }, + "domain": { + Type: schema.TypeString, + Description: "The domain to be used for authentication", + Required: true, + }, + "app_id": { + Type: schema.TypeString, + Description: "The Codefresh application ID in your Onelogin", + Optional: true, + }, + "api_client_id": { + Type: schema.TypeString, + Description: "Client ID for onelogin API, only needed if syncing users and groups from Onelogin", + Optional: true, + }, + "api_client_secret": { + Type: schema.TypeString, + Description: "Client secret for onelogin API, only needed if syncing users and groups from Onelogin", + Optional: true, + // When onelogin IDP is created on account level, after the first apply the client secret is returned obfuscated + // DiffSuppressFunc: surpressObfuscatedFields(), + }, + }, + }, + }, + "keycloak": { + Description: "Settings for Keycloak IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "client_id": { + Type: schema.TypeString, + Description: "Client ID from Keycloak", + Required: true, + }, + "client_secret": { + Type: schema.TypeString, + Description: "Client secret from Keycloak", + Required: true, + Sensitive: true, + }, + "host": { + Type: schema.TypeString, + Description: "The Keycloak URL", + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^(https?:\/\/)(\S+)$`), "must be a valid url"), + }, + "realm": { + Type: schema.TypeString, + Description: "The Realm ID for Codefresh in Keycloak. Defaults to master", + Optional: true, + Default: "master", + }, + }, + }, + }, + "saml": { + Description: "Settings for SAML IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint": { + Type: schema.TypeString, + Description: "The SSO endpoint of your Identity Provider", + Required: true, + }, + "application_certificate": { + Type: schema.TypeString, + Description: "The security certificate of your Identity Provider. Paste the value directly on the field. Do not convert to base64 or any other encoding by hand", + Required: true, + Sensitive: true, + }, + "provider": { + Type: schema.TypeString, + Description: "SAML provider. Currently supported values - GSuite, okta or empty string for generic provider. Defaults to empty string", + Optional: true, + Default: "", + ValidateFunc: validation.StringInSlice([]string{"", "okta", "GSuite"}, false), + }, + "allowed_groups_for_sync": { + Type: schema.TypeString, + Description: "Valid for GSuite only: Comma separated list of groups to sync", + Optional: true, + }, + "autosync_teams_and_users": { + Type: schema.TypeBool, + Description: "Valid for Okta/GSuite: Set to true to sync user accounts and teams in okta/gsuite to your Codefresh account", + Optional: true, + Default: false, + }, + "sync_interval": { + Type: schema.TypeInt, + Description: "Valid for Okta/GSuite: Sync interval in hours for syncing user accounts in okta/gsuite to your Codefresh account. If not set the sync inteval will be 12 hours", + Optional: true, + }, + "activate_users_after_sync": { + Type: schema.TypeBool, + Description: "Valid for Okta only: If set to true, Codefresh will automatically invite and activate new users added during the automated sync, without waiting for the users to accept the invitations. Defaults to false", + Optional: true, + Default: false, + }, + "app_id": { + Type: schema.TypeString, + Description: "Valid for Okta only: The Codefresh application ID in Okta", + Optional: true, + }, + "client_host": { + Type: schema.TypeString, + Description: "Valid for Okta only: OKTA organization URL, for example, https://.okta.com", + Optional: true, + }, + "json_keyfile": { + Type: schema.TypeString, + Description: "Valid for GSuite only: JSON keyfile for google service account used for synchronization", + Optional: true, + }, + "admin_email": { + Type: schema.TypeString, + Description: "Valid for GSuite only: Email of a user with admin permissions on google, relevant only for synchronization", + Optional: true, + }, + "access_token": { + Type: schema.TypeString, + Description: "Valid for Okta only: The Okta API token generated in Okta, used to sync groups and their users from Okta to Codefresh", + Optional: true, + }, + }, + }, + }, + "ldap": { + Description: "Settings for Keycloak IDP", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: SupportedIdps, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "url": { + Type: schema.TypeString, + Description: "ldap server url", + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^ldap(s?):\/\/`), "must be a valid ldap url (must start with ldap:// or ldaps://)"), + }, + "password": { + Type: schema.TypeString, + Description: "The password of the user defined in Distinguished name that will be used to search other users", + Required: true, + Sensitive: true, + }, + "distinguished_name": { + Type: schema.TypeString, + Description: "The username to be used to search other users in LDAP notation (combination of cn, ou,dc)", + Optional: true, + Computed: true, + }, + "search_base": { + Type: schema.TypeString, + Description: "The search-user scope in LDAP notation", + Required: true, + }, + "search_filter": { + Type: schema.TypeString, + Description: "The attribute by which to search for the user on the LDAP server. By default, set to uid. For the Azure LDAP server, set this field to sAMAccountName", + Optional: true, + }, + "certificate": { + Type: schema.TypeString, + Description: "For ldaps only: The security certificate of the LDAP server. Do not convert to base64 or any other encoding", + Optional: true, + }, + "allowed_groups_for_sync": { + Type: schema.TypeString, + Description: "To sync only by specified groups - specify a comma separated list of groups, by default all groups will be synced", + Optional: true, + }, + "search_base_for_sync": { + Type: schema.TypeString, + Description: "Synchronize using a custom search base, by deafult seach_base is used", + Optional: true, + }, + }, + }, + }, + } +) diff --git a/codefresh/internal/idp/types.go b/codefresh/internal/idp/types.go new file mode 100644 index 0000000..feaff76 --- /dev/null +++ b/codefresh/internal/idp/types.go @@ -0,0 +1,15 @@ +package idp + +const ( + GitHub string = "github" + GitLab string = "gitlab" + Bitbucket string = "bitbucket" + Okta string = "okta" + Google string = "google" + Auth0 string = "auth0" + Azure string = "azure" + OneLogin string = "onelogin" + Keycloak string = "keycloak" + SAML string = "saml" + LDAP string = "ldap" +) diff --git a/codefresh/provider.go b/codefresh/provider.go index 0a41589..2f77def 100644 --- a/codefresh/provider.go +++ b/codefresh/provider.go @@ -51,6 +51,7 @@ func Provider() *schema.Provider { "codefresh_users": dataSourceUsers(), "codefresh_registry": dataSourceRegistry(), "codefresh_pipelines": dataSourcePipelines(), + "codefresh_account_idp": dataSourceAccountIdp(), "codefresh_project": dataSourceProject(), }, ResourcesMap: map[string]*schema.Resource{ @@ -69,6 +70,8 @@ func Provider() *schema.Provider { "codefresh_user": resourceUser(), "codefresh_team": resourceTeam(), "codefresh_abac_rules": resourceGitopsAbacRule(), + "codefresh_idp": resourceIdp(), + "codefresh_account_idp": resourceAccountIdp(), }, ConfigureFunc: configureProvider, } diff --git a/codefresh/resource_account_idp.go b/codefresh/resource_account_idp.go new file mode 100644 index 0000000..025215d --- /dev/null +++ b/codefresh/resource_account_idp.go @@ -0,0 +1,398 @@ +package codefresh + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/internal/datautil" + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/internal/idp" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceAccountIdp() *schema.Resource { + return &schema.Resource{ + Description: "Account level identity providers", + Create: resourceAccountIDPCreate, + Read: resourceAccountIDPRead, + Update: resourceAccountIDPUpdate, + Delete: resourceAccountIDPDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + CustomizeDiff: customdiff.All( + // Recreate idp if the type has changed - we cannot simply do ForceNew on client_type as it is computed + customdiff.ForceNewIf("client_type", func(_ context.Context, d *schema.ResourceDiff, meta interface{}) bool { + clientTypeInState := d.Get("client_type").(string) + attributesForIdpTypeInState := d.Get(clientTypeInState) + // If there is a different type of idp in the state, the idp needs to be recreated + if attributesForIdpTypeInState == nil { + d.SetNewComputed("client_type") + return true + } else if len(attributesForIdpTypeInState.([]interface{})) < 1 { + d.SetNewComputed("client_type") + return true + } else { + return false + } + }), + // If name has changed for an account scoped IDP the provider needs to ignore it as the API always generates the name + customdiff.If(func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { + return d.HasChange("name") + }, + func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { + old, _ := d.GetChange("name") + if err := d.SetNew("name", old); err != nil { + return err + } + return nil + }), + ), + // Defined in resource_idp, as schema is the same for global and account scoped IDPs + Schema: idp.IdpSchema, + } +} + +func resourceAccountIDPCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + id, err := client.CreateIDP(mapResourceToAccountIDP(d), false) + if err != nil { + log.Printf("[DEBUG] Error while creating idp. Error = %v", err) + return err + } + + d.SetId(id) + return resourceIDPRead(d, meta) +} + +func resourceAccountIDPRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + idpID := d.Id() + + var cfClientIDP *cfclient.IDP + var err error + + cfClientIDP, err = client.GetAccountIdpByID(idpID) + if err != nil { + if err.Error() == fmt.Sprintf("[ERROR] IDP with ID %s isn't found.", d.Id()) { + d.SetId("") + return nil + } + log.Printf("[DEBUG] Error while getting IDP. Error = %v", err) + return err + + } + + err = mapAccountIDPToResource(*cfClientIDP, d) + if err != nil { + log.Printf("[DEBUG] Error while getting mapping response to IDP object. Error = %v", err) + return err + } + + return nil +} + +func resourceAccountIDPDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + err := client.DeleteIDPAccount(d.Id()) + if err != nil { + log.Printf("[DEBUG] Error while deleting account level IDP. Error = %v", err) + return err + } + + return nil +} + +func resourceAccountIDPUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cfclient.Client) + + err := client.UpdateIDP(mapResourceToAccountIDP(d), false) + if err != nil { + log.Printf("[DEBUG] Error while updating idp. Error = %v", err) + return err + } + + return resourceIDPRead(d, meta) +} + +func mapAccountIDPToResource(cfClientIDP cfclient.IDP, d *schema.ResourceData) error { + d.SetId(cfClientIDP.ID) + d.Set("display_name", cfClientIDP.DisplayName) + d.Set("name", cfClientIDP.ClientName) + d.Set("redirect_url", cfClientIDP.RedirectUrl) + d.Set("redirect_ui_url", cfClientIDP.RedirectUiUrl) + d.Set("login_url", cfClientIDP.LoginUrl) + d.Set("client_type", cfClientIDP.ClientType) + + if cfClientIDP.ClientType == idp.GitHub { + attributes := []map[string]interface{}{{ + "client_id": cfClientIDP.ClientId, + // Codefresh API Returns the client secret as an encrypted string on the server side + // hence we need to keep in the state the original secret the user provides along with the encrypted computed secret + // for Terraform to properly calculate the diff + "client_secret": d.Get("github.0.client_secret"), + "authentication_url": cfClientIDP.AuthURL, + "token_url": cfClientIDP.TokenURL, + "user_profile_url": cfClientIDP.UserProfileURL, + "api_host": cfClientIDP.ApiHost, + "api_path_prefix": cfClientIDP.ApiPathPrefix, + }} + + d.Set(idp.GitHub, attributes) + } + + if cfClientIDP.ClientType == idp.GitLab { + attributes := []map[string]interface{}{{ + "client_id": cfClientIDP.ClientId, + "client_secret": d.Get("gitlab.0.client_secret"), + "authentication_url": cfClientIDP.AuthURL, + "user_profile_url": cfClientIDP.UserProfileURL, + "api_url": cfClientIDP.ApiURL, + }} + + d.Set(idp.GitLab, attributes) + } + + if cfClientIDP.ClientType == idp.Okta { + attributes := []map[string]interface{}{{ + "client_id": cfClientIDP.ClientId, + "client_secret": d.Get("okta.0.client_secret"), + "client_host": cfClientIDP.ClientHost, + "app_id": d.Get("okta.0.app_id"), + "sync_mirror_accounts": cfClientIDP.SyncMirrorAccounts, + "access_token": d.Get("okta.0.access_token"), + }} + + d.Set("okta", attributes) + } + + if cfClientIDP.ClientType == idp.Google { + attributes := []map[string]interface{}{{ + "client_id": cfClientIDP.ClientId, + "client_secret": d.Get("google.0.client_secret"), + "admin_email": d.Get("google.0.admin_email"), + "json_keyfile": d.Get("google.0.json_keyfile"), + "allowed_groups_for_sync": cfClientIDP.AllowedGroupsForSync, + "sync_field": cfClientIDP.SyncField, + }} + + d.Set(idp.Google, attributes) + } + + if cfClientIDP.ClientType == idp.Auth0 { + attributes := []map[string]interface{}{{ + "client_id": cfClientIDP.ClientId, + "client_secret": d.Get("auth0.0.client_secret"), + "domain": cfClientIDP.ClientHost, + }} + + d.Set(idp.Auth0, attributes) + } + + if cfClientIDP.ClientType == idp.Azure { + + syncInterval, err := strconv.Atoi(cfClientIDP.SyncInterval) + if err != nil { + return err + } + + attributes := []map[string]interface{}{{ + "app_id": cfClientIDP.ClientId, + "client_secret": d.Get("azure.0.client_secret"), + "object_id": cfClientIDP.AppId, + "autosync_teams_and_users": cfClientIDP.AutoGroupSync, + "sync_interval": syncInterval, + "tenant": cfClientIDP.Tenant, + }} + + d.Set(idp.Azure, attributes) + } + + if cfClientIDP.ClientType == idp.OneLogin { + attributes := []map[string]interface{}{{ + "client_id": cfClientIDP.ClientId, + "client_secret": d.Get("onelogin.0.client_secret"), + "domain": cfClientIDP.ClientHost, + "api_client_id": cfClientIDP.ApiClientId, + // When account scoped, Client secret is returned obfuscated after first apply, causing diff to appear everytime. + // This behavior would always set the API clint secret from the resource, allowing at least changing the secret when the value in terraform configuration changes. + // Though it would not detect drift if the secret is changed from UI. + "api_client_secret": d.Get("onelogin.0.api_client_secret"), + "app_id": cfClientIDP.AppId, + }} + + d.Set(idp.OneLogin, attributes) + } + + if cfClientIDP.ClientType == idp.Keycloak { + attributes := []map[string]interface{}{{ + "client_id": cfClientIDP.ClientId, + "client_secret": d.Get("keycloak.0.client_secret"), + "host": cfClientIDP.Host, + "realm": cfClientIDP.Realm, + }} + + d.Set(idp.Keycloak, attributes) + } + + if cfClientIDP.ClientType == idp.SAML { + syncInterval, err := strconv.Atoi(cfClientIDP.SyncInterval) + if err != nil { + return err + } + attributes := []map[string]interface{}{{ + "endpoint": cfClientIDP.EntryPoint, + "application_certificate": d.Get("saml.0.application_certificate"), + "provider": cfClientIDP.SamlProvider, + "allowed_groups_for_sync": cfClientIDP.AllowedGroupsForSync, + "autosync_teams_and_users": cfClientIDP.AutoGroupSync, + "activate_users_after_sync": cfClientIDP.ActivateUserAfterSync, + "sync_interval": syncInterval, + "app_id": cfClientIDP.AppId, + "client_host": cfClientIDP.ClientHost, + "json_keyfile": d.Get("saml.0.json_keyfile"), + "admin_email": d.Get("saml.0.admin_email"), + "access_token": d.Get("saml.0.access_token"), + }} + + d.Set(idp.SAML, attributes) + } + + if cfClientIDP.ClientType == idp.LDAP { + attributes := []map[string]interface{}{{ + "url": cfClientIDP.Url, + "password": d.Get("ldap.0.password"), + "distinguished_name": cfClientIDP.DistinguishedName, + "search_base": cfClientIDP.SearchBase, + "search_filter": cfClientIDP.SearchFilter, + "certificate": d.Get("ldap.0.certificate"), + "allowed_groups_for_sync": cfClientIDP.AllowedGroupsForSync, + "search_base_for_sync": cfClientIDP.SearchBaseForSync, + }} + + d.Set(idp.LDAP, attributes) + } + + return nil +} + +func mapResourceToAccountIDP(d *schema.ResourceData) *cfclient.IDP { + cfClientIDP := &cfclient.IDP{ + ID: d.Id(), + DisplayName: d.Get("display_name").(string), + ClientName: d.Get("name").(string), + RedirectUrl: d.Get("redirect_url").(string), + RedirectUiUrl: d.Get("redirect_ui_url").(string), + LoginUrl: d.Get("login_url").(string), + } + + if _, ok := d.GetOk(idp.GitHub); ok { + cfClientIDP.ClientType = idp.GitHub + cfClientIDP.ClientId = d.Get("github.0.client_id").(string) + cfClientIDP.ClientSecret = d.Get("github.0.client_secret").(string) + cfClientIDP.AuthURL = d.Get("github.0.authentication_url").(string) + cfClientIDP.TokenURL = d.Get("github.0.token_url").(string) + cfClientIDP.UserProfileURL = d.Get("github.0.user_profile_url").(string) + cfClientIDP.ApiHost = d.Get("github.0.api_host").(string) + cfClientIDP.ApiPathPrefix = d.Get("github.0.api_path_prefix").(string) + } + + if _, ok := d.GetOk(idp.GitLab); ok { + cfClientIDP.ClientType = idp.GitLab + cfClientIDP.ClientId = d.Get("gitlab.0.client_id").(string) + cfClientIDP.ClientSecret = d.Get("gitlab.0.client_secret").(string) + cfClientIDP.AuthURL = d.Get("gitlab.0.authentication_url").(string) + cfClientIDP.UserProfileURL = d.Get("gitlab.0.user_profile_url").(string) + cfClientIDP.ApiURL = d.Get("gitlab.0.api_url").(string) + } + + if _, ok := d.GetOk(idp.Okta); ok { + cfClientIDP.ClientType = idp.Okta + cfClientIDP.ClientId = d.Get("okta.0.client_id").(string) + cfClientIDP.ClientSecret = d.Get("okta.0.client_secret").(string) + cfClientIDP.ClientHost = d.Get("okta.0.client_host").(string) + cfClientIDP.AppId = d.Get("okta.0.app_id").(string) + cfClientIDP.SyncMirrorAccounts = datautil.ConvertStringArr(d.Get("okta.0.sync_mirror_accounts").([]interface{})) + cfClientIDP.Access_token = d.Get("okta.0.access_token").(string) + } + + if _, ok := d.GetOk(idp.Google); ok { + cfClientIDP.ClientType = idp.Google + cfClientIDP.ClientId = d.Get("google.0.client_id").(string) + cfClientIDP.ClientSecret = d.Get("google.0.client_secret").(string) + cfClientIDP.KeyFile = d.Get("google.0.json_keyfile").(string) + cfClientIDP.Subject = d.Get("google.0.admin_email").(string) + cfClientIDP.AllowedGroupsForSync = d.Get("google.0.allowed_groups_for_sync").(string) + cfClientIDP.SyncField = d.Get("google.0.sync_field").(string) + } + + if _, ok := d.GetOk(idp.Auth0); ok { + cfClientIDP.ClientType = idp.Auth0 + cfClientIDP.ClientId = d.Get("auth0.0.client_id").(string) + cfClientIDP.ClientSecret = d.Get("auth0.0.client_secret").(string) + cfClientIDP.ClientHost = d.Get("auth0.0.domain").(string) + } + + if _, ok := d.GetOk(idp.Azure); ok { + cfClientIDP.ClientType = idp.Azure + cfClientIDP.ClientId = d.Get("azure.0.app_id").(string) + cfClientIDP.ClientSecret = d.Get("azure.0.client_secret").(string) + cfClientIDP.AppId = d.Get("azure.0.object_id").(string) + cfClientIDP.Tenant = d.Get("azure.0.tenant").(string) + cfClientIDP.AutoGroupSync = d.Get("azure.0.autosync_teams_and_users").(bool) + cfClientIDP.SyncInterval = strconv.Itoa(d.Get("azure.0.sync_interval").(int)) + } + + if _, ok := d.GetOk(idp.OneLogin); ok { + cfClientIDP.ClientType = idp.OneLogin + cfClientIDP.ClientId = d.Get("onelogin.0.client_id").(string) + cfClientIDP.ClientSecret = d.Get("onelogin.0.client_secret").(string) + cfClientIDP.ClientHost = d.Get("onelogin.0.domain").(string) + cfClientIDP.AppId = d.Get("onelogin.0.app_id").(string) + cfClientIDP.ApiClientId = d.Get("onelogin.0.api_client_id").(string) + cfClientIDP.ApiClientSecret = d.Get("onelogin.0.api_client_secret").(string) + } + + if _, ok := d.GetOk(idp.Keycloak); ok { + cfClientIDP.ClientType = idp.Keycloak + cfClientIDP.ClientId = d.Get("keycloak.0.client_id").(string) + cfClientIDP.ClientSecret = d.Get("keycloak.0.client_secret").(string) + cfClientIDP.Host = d.Get("keycloak.0.host").(string) + cfClientIDP.Realm = d.Get("keycloak.0.realm").(string) + } + + if _, ok := d.GetOk(idp.SAML); ok { + cfClientIDP.ClientType = idp.SAML + cfClientIDP.SamlProvider = d.Get("saml.0.provider").(string) + cfClientIDP.EntryPoint = d.Get("saml.0.endpoint").(string) + cfClientIDP.ApplicationCert = d.Get("saml.0.application_certificate").(string) + cfClientIDP.AllowedGroupsForSync = d.Get("saml.0.allowed_groups_for_sync").(string) + cfClientIDP.AutoGroupSync = d.Get("saml.0.autosync_teams_and_users").(bool) + cfClientIDP.ActivateUserAfterSync = d.Get("saml.0.activate_users_after_sync").(bool) + cfClientIDP.SyncInterval = strconv.Itoa(d.Get("saml.0.sync_interval").(int)) + cfClientIDP.AppId = d.Get("saml.0.app_id").(string) + cfClientIDP.ClientHost = d.Get("saml.0.client_host").(string) + cfClientIDP.KeyFile = d.Get("saml.0.json_keyfile").(string) + cfClientIDP.Subject = d.Get("saml.0.admin_email").(string) + cfClientIDP.Access_token = d.Get("saml.0.access_token").(string) + } + + if _, ok := d.GetOk(idp.LDAP); ok { + cfClientIDP.ClientType = idp.LDAP + cfClientIDP.Url = d.Get("ldap.0.url").(string) + cfClientIDP.Password = d.Get("ldap.0.password").(string) + cfClientIDP.DistinguishedName = d.Get("ldap.0.distinguished_name").(string) + cfClientIDP.SearchBase = d.Get("ldap.0.search_base").(string) + cfClientIDP.SearchFilter = d.Get("ldap.0.search_filter").(string) + cfClientIDP.Certificate = d.Get("ldap.0.certificate").(string) + cfClientIDP.AllowedGroupsForSync = d.Get("ldap.0.allowed_groups_for_sync").(string) + cfClientIDP.SearchBaseForSync = d.Get("ldap.0.search_base_for_sync").(string) + } + + return cfClientIDP +} diff --git a/codefresh/resource_account_idp_test.go b/codefresh/resource_account_idp_test.go new file mode 100644 index 0000000..7aca71f --- /dev/null +++ b/codefresh/resource_account_idp_test.go @@ -0,0 +1,278 @@ +package codefresh + +import ( + "fmt" + "testing" + + "github.com/codefresh-io/terraform-provider-codefresh/codefresh/cfclient" + "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/terraform" +) + +// Check create, update and delete of all supported IDP types +func TestAccountIDPCodefreshProject_AllSupportedTypes(t *testing.T) { + uniqueId := acctest.RandString(10) + resourceName := "codefresh_account_idp.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + //CheckDestroy: testAccCheckCodefreshProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccountIDPCodefreshConfig("onelogin", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-onelogin-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "onelogin"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + // For existing resources codefresh returns the secrets in encrypted format, to avoid constant diff we store those in _encrypted, hence on import the secrets will be empty + ImportStateVerifyIgnore: []string{"onelogin.0.client_secret"}, + }, + { + Config: testAccountIDPCodefreshConfig("auth0", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-auth0-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "auth0"), + ), + }, + { + Config: testAccountIDPCodefreshConfig("azure", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-azure-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "azure"), + ), + }, + { + Config: testAccountIDPCodefreshConfig("google", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-google-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "google"), + ), + }, + { + Config: testAccountIDPCodefreshConfig("keycloak", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-keycloak-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "keycloak"), + ), + }, + { + Config: testAccountIDPCodefreshConfig("ldap", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-ldap-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "ldap"), + ), + }, + { + Config: testAccountIDPCodefreshConfig("okta", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-okta-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "okta"), + ), + }, + { + Config: testAccountIDPCodefreshConfig("saml", uniqueId), + Check: resource.ComposeTestCheckFunc( + testAccCheckCodefreshAccountIDPExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "display_name", fmt.Sprintf("tf-test-saml-%s", uniqueId)), + resource.TestCheckResourceAttr(resourceName, "client_type", "saml"), + ), + }, + }, + }) +} + +func testAccCheckCodefreshAccountIDPExists(resource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Not found: %s", resource) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + idpID := rs.Primary.ID + + apiClient := testAccProvider.Meta().(*cfclient.Client) + _, err := apiClient.GetAccountIdpByID(idpID) + + if err != nil { + return fmt.Errorf("error fetching project with resource %s. %s", resource, err) + } + return nil + } +} + +func testAccountIDPCodefreshConfig(idpType string, uniqueId string) string { + + idpResource := "" + + if idpType == "onelogin" { + idpResource = fmt.Sprintf(` + resource "codefresh_account_idp" "test" { + display_name = "tf-test-onelogin-%s" + + onelogin { + client_id = "onelogin-%s" + client_secret = "myoneloginsecret1" + domain = "myonelogindomain" + app_id = "myappid" + api_client_id = "myonelogindomain" + api_client_secret = "myapiclientsecret1" + } + }`, uniqueId, uniqueId) + } + + if idpType == "auth0" { + idpResource = fmt.Sprintf(` + resource "codefresh_account_idp" "test" { + display_name = "tf-test-auth0-%s" + name = "tf-auth0-test34" + auth0 { + client_id = "blah-auth0-%s" + client_secret = "asdddd" + domain = "codefresh.auth0.com" + } + }`, uniqueId, uniqueId) + } + + if idpType == "azure" { + idpResource = fmt.Sprintf(` + resource "codefresh_account_idp" "test" { + display_name = "tf-test-azure-%s" + name = "tf-azure-test" + azure { + app_id = "azure-codefresh-test-%s" + client_secret = "mysecret99" + object_id = "myobjectidtest" + autosync_teams_and_users = true + sync_interval = 7 + } + }`, uniqueId, uniqueId) + } + + if idpType == "google" { + idpResource = fmt.Sprintf(` + resource "codefresh_account_idp" "test" { + display_name = "tf-test-google-%s" + name = "tf-google-test" + google { + client_id = "tf-test-google-%s" + client_secret = "mysecret99" + admin_email = "admin@codefresh.io" + sync_field = "myfield" + json_keyfile = < +## Schema + +### Optional + +- `_id` (String) +- `client_name` (String) + +### Read-Only + +- `client_type` (String) +- `display_name` (String) +- `id` (String) The ID of this resource. +- `login_url` (String) Login url using the IDP to Codefresh +- `redirect_ui_url` (String) UI Callback url for the identity provider +- `redirect_url` (String) API Callback url for the identity provider + + diff --git a/docs/guides/development.md b/docs/guides/development.md index d1369fa..07d0051 100644 --- a/docs/guides/development.md +++ b/docs/guides/development.md @@ -21,7 +21,7 @@ Set the [developer overrides](https://developer.hashicorp.com/terraform/cli/conf # `~/.terraformrc (Windows: %APPDATA%/.terraformrc) provider_installation { dev_overrides { - "codefresh.io/codefresh" = "[REPLACE WITH GOPATH]/bin" + "codefresh-io/codefresh" = "[REPLACE WITH GOPATH]/bin" } direct {} } diff --git a/docs/resources/account_idp.md b/docs/resources/account_idp.md new file mode 100644 index 0000000..b5f897d --- /dev/null +++ b/docs/resources/account_idp.md @@ -0,0 +1,243 @@ +--- +page_title: "codefresh_account_idp Resource - terraform-provider-codefresh" +subcategory: "" +description: |- + Account level identity providers +--- + +# codefresh_account_idp (Resource) + +Account level identity providers + +## Example usage +```hcl +resource "codefresh_account_idp" "auth0-test" { + display_name = "tf-auth0-example" + + auth0 { + client_id = "auht0-codefresh-example" + client_secret = "mysecret" + domain = "codefresh.auth0.com" + } +} +``` +```hcl +resource "codefresh_account_idp" "google-example" { + display_name = "tf-google-example" + + google { + client_id = "google-codefresh-example" + client_secret = "mysecret99" + admin_email = "admin@codefresh.io" + sync_field = "myfield" + json_keyfile = < +## Schema + +### Required + +- `display_name` (String) The display name for the IDP. + +### Optional + +- `auth0` (Block List, Max: 1) Settings for Auth0 IDP (see [below for nested schema](#nestedblock--auth0)) +- `azure` (Block List, Max: 1) Settings for Azure IDP (see [below for nested schema](#nestedblock--azure)) +- `github` (Block List, Max: 1) Settings for GitHub IDP (see [below for nested schema](#nestedblock--github)) +- `gitlab` (Block List, Max: 1) Settings for GitLab IDP (see [below for nested schema](#nestedblock--gitlab)) +- `google` (Block List, Max: 1) Settings for Google IDP (see [below for nested schema](#nestedblock--google)) +- `keycloak` (Block List, Max: 1) Settings for Keycloak IDP (see [below for nested schema](#nestedblock--keycloak)) +- `ldap` (Block List, Max: 1) Settings for Keycloak IDP (see [below for nested schema](#nestedblock--ldap)) +- `name` (String) Name of the IDP, will be generated if not set +- `okta` (Block List, Max: 1) Settings for Okta IDP (see [below for nested schema](#nestedblock--okta)) +- `onelogin` (Block List, Max: 1) Settings for onelogin IDP (see [below for nested schema](#nestedblock--onelogin)) +- `saml` (Block List, Max: 1) Settings for SAML IDP (see [below for nested schema](#nestedblock--saml)) + +### Read-Only + +- `client_type` (String) Type of the IDP. Derived from idp specific config object (github, gitlab etc) +- `id` (String) The ID of this resource. +- `login_url` (String) Login url using the IDP to Codefresh +- `redirect_ui_url` (String) UI Callback url for the identity provider +- `redirect_url` (String) API Callback url for the identity provider + + +### Nested Schema for `auth0` + +Required: + +- `client_id` (String) Client ID from Auth0 +- `client_secret` (String, Sensitive) Client secret from Auth0 +- `domain` (String) The domain of the Auth0 application + + + +### Nested Schema for `azure` + +Required: + +- `app_id` (String) The Application ID from your Enterprise Application Properties in Azure AD +- `client_secret` (String, Sensitive) Client secret from Azure + +Optional: + +- `autosync_teams_and_users` (Boolean) Set to true to sync user accounts in Azure AD to your Codefresh account +- `object_id` (String) The Object ID from your Enterprise Application Properties in Azure AD +- `sync_interval` (Number) Sync interval in hours for syncing user accounts in Azure AD to your Codefresh account. If not set the sync inteval will be 12 hours +- `tenant` (String) Azure tenant + + + +### Nested Schema for `github` + +Required: + +- `client_id` (String) Client ID from Github +- `client_secret` (String, Sensitive) Client secret from GitHub + +Optional: + +- `api_host` (String) GitHub API host, Defaults to api.github.com +- `api_path_prefix` (String) GitHub API url path prefix, defaults to / +- `authentication_url` (String) Authentication url, Defaults to https://github.com/login/oauth/authorize +- `token_url` (String) GitHub token endpoint url, Defaults to https://github.com/login/oauth/access_token +- `user_profile_url` (String) GitHub user profile url, Defaults to https://api.github.com/user + + + +### Nested Schema for `gitlab` + +Required: + +- `client_id` (String) Client ID from Gitlab +- `client_secret` (String, Sensitive) Client secret from Gitlab + +Optional: + +- `api_url` (String) Base url for Gitlab API, Defaults to https://gitlab.com/api/v4/ +- `authentication_url` (String) Authentication url, Defaults to https://gitlab.com +- `user_profile_url` (String) User profile url, Defaults to https://gitlab.com/api/v4/user + + + +### Nested Schema for `google` + +Required: + +- `client_id` (String) Client ID in Google, must be unique across all identity providers in Codefresh +- `client_secret` (String, Sensitive) Client secret in Google + +Optional: + +- `admin_email` (String) Email of a user with admin permissions on google, relevant only for synchronization +- `allowed_groups_for_sync` (String) Comma separated list of groups to sync +- `json_keyfile` (String) JSON keyfile for google service account used for synchronization +- `sync_field` (String) Relevant for custom schema-based synchronization only. See Codefresh documentation + + + +### Nested Schema for `keycloak` + +Required: + +- `client_id` (String) Client ID from Keycloak +- `client_secret` (String, Sensitive) Client secret from Keycloak +- `host` (String) The Keycloak URL + +Optional: + +- `realm` (String) The Realm ID for Codefresh in Keycloak. Defaults to master + + + +### Nested Schema for `ldap` + +Required: + +- `password` (String, Sensitive) The password of the user defined in Distinguished name that will be used to search other users +- `search_base` (String) The search-user scope in LDAP notation +- `url` (String) ldap server url + +Optional: + +- `allowed_groups_for_sync` (String) To sync only by specified groups - specify a comma separated list of groups, by default all groups will be synced +- `certificate` (String) For ldaps only: The security certificate of the LDAP server. Do not convert to base64 or any other encoding +- `distinguished_name` (String) The username to be used to search other users in LDAP notation (combination of cn, ou,dc) +- `search_base_for_sync` (String) Synchronize using a custom search base, by deafult seach_base is used +- `search_filter` (String) The attribute by which to search for the user on the LDAP server. By default, set to uid. For the Azure LDAP server, set this field to sAMAccountName + + + +### Nested Schema for `okta` + +Required: + +- `client_host` (String) The OKTA organization URL, for example, https://.okta.com +- `client_id` (String) Client ID in Okta, must be unique across all identity providers in Codefresh +- `client_secret` (String, Sensitive) Client secret in Okta + +Optional: + +- `access_token` (String) The Okta API token generated in Okta, used to sync groups and their users from Okta to Codefresh +- `app_id` (String) The Codefresh application ID in your OKTA organization +- `sync_mirror_accounts` (List of String) The names of the additional Codefresh accounts to be synced from Okta + + + +### Nested Schema for `onelogin` + +Required: + +- `client_id` (String) Client ID from Onelogin +- `client_secret` (String, Sensitive) Client secret from Onelogin +- `domain` (String) The domain to be used for authentication + +Optional: + +- `api_client_id` (String) Client ID for onelogin API, only needed if syncing users and groups from Onelogin +- `api_client_secret` (String) Client secret for onelogin API, only needed if syncing users and groups from Onelogin +- `app_id` (String) The Codefresh application ID in your Onelogin + + + +### Nested Schema for `saml` + +Required: + +- `application_certificate` (String, Sensitive) The security certificate of your Identity Provider. Paste the value directly on the field. Do not convert to base64 or any other encoding by hand +- `endpoint` (String) The SSO endpoint of your Identity Provider + +Optional: + +- `access_token` (String) Valid for Okta only: The Okta API token generated in Okta, used to sync groups and their users from Okta to Codefresh +- `activate_users_after_sync` (Boolean) Valid for Okta only: If set to true, Codefresh will automatically invite and activate new users added during the automated sync, without waiting for the users to accept the invitations. Defaults to false +- `admin_email` (String) Valid for GSuite only: Email of a user with admin permissions on google, relevant only for synchronization +- `allowed_groups_for_sync` (String) Valid for GSuite only: Comma separated list of groups to sync +- `app_id` (String) Valid for Okta only: The Codefresh application ID in Okta +- `autosync_teams_and_users` (Boolean) Valid for Okta/GSuite: Set to true to sync user accounts and teams in okta/gsuite to your Codefresh account +- `client_host` (String) Valid for Okta only: OKTA organization URL, for example, https://.okta.com +- `json_keyfile` (String) Valid for GSuite only: JSON keyfile for google service account used for synchronization +- `provider` (String) SAML provider. Currently supported values - GSuite, okta or empty string for generic provider. Defaults to empty string +- `sync_interval` (Number) Valid for Okta/GSuite: Sync interval in hours for syncing user accounts in okta/gsuite to your Codefresh account. If not set the sync inteval will be 12 hours + +## Import + +Please note that secret fields are not imported. +
All secrets should be provided in the configuration and applied after the import for the state to be consistent. + +```sh +terraform import codefresh_account_idp.test +``` diff --git a/docs/resources/idp.md b/docs/resources/idp.md new file mode 100644 index 0000000..b05d1e5 --- /dev/null +++ b/docs/resources/idp.md @@ -0,0 +1,243 @@ +--- +page_title: "codefresh_idp Resource - terraform-provider-codefresh" +subcategory: "" +description: |- + Codefresh global level identity provider. Requires Codefresh admin token, hence is relevant only for on-prem deployments of Codefresh +--- + +# codefresh_idp (Resource) + +Codefresh global level identity provider. Requires Codefresh admin token, hence is relevant only for on-prem deployments of Codefresh + +## Example usage +```hcl +resource "codefresh_idp" "auth0-test" { + display_name = "tf-auth0-example" + + auth0 { + client_id = "auht0-codefresh-example" + client_secret = "mysecret" + domain = "codefresh.auth0.com" + } +} +``` +```hcl +resource "codefresh_idp" "google-example" { + display_name = "tf-google-example" + + google { + client_id = "google-codefresh-example" + client_secret = "mysecret99" + admin_email = "admin@codefresh.io" + sync_field = "myfield" + json_keyfile = < +## Schema + +### Required + +- `display_name` (String) The display name for the IDP. + +### Optional + +- `auth0` (Block List, Max: 1) Settings for Auth0 IDP (see [below for nested schema](#nestedblock--auth0)) +- `azure` (Block List, Max: 1) Settings for Azure IDP (see [below for nested schema](#nestedblock--azure)) +- `github` (Block List, Max: 1) Settings for GitHub IDP (see [below for nested schema](#nestedblock--github)) +- `gitlab` (Block List, Max: 1) Settings for GitLab IDP (see [below for nested schema](#nestedblock--gitlab)) +- `google` (Block List, Max: 1) Settings for Google IDP (see [below for nested schema](#nestedblock--google)) +- `keycloak` (Block List, Max: 1) Settings for Keycloak IDP (see [below for nested schema](#nestedblock--keycloak)) +- `ldap` (Block List, Max: 1) Settings for Keycloak IDP (see [below for nested schema](#nestedblock--ldap)) +- `name` (String) Name of the IDP, will be generated if not set +- `okta` (Block List, Max: 1) Settings for Okta IDP (see [below for nested schema](#nestedblock--okta)) +- `onelogin` (Block List, Max: 1) Settings for onelogin IDP (see [below for nested schema](#nestedblock--onelogin)) +- `saml` (Block List, Max: 1) Settings for SAML IDP (see [below for nested schema](#nestedblock--saml)) + +### Read-Only + +- `client_type` (String) Type of the IDP. Derived from idp specific config object (github, gitlab etc) +- `id` (String) The ID of this resource. +- `login_url` (String) Login url using the IDP to Codefresh +- `redirect_ui_url` (String) UI Callback url for the identity provider +- `redirect_url` (String) API Callback url for the identity provider + + +### Nested Schema for `auth0` + +Required: + +- `client_id` (String) Client ID from Auth0 +- `client_secret` (String, Sensitive) Client secret from Auth0 +- `domain` (String) The domain of the Auth0 application + + + +### Nested Schema for `azure` + +Required: + +- `app_id` (String) The Application ID from your Enterprise Application Properties in Azure AD +- `client_secret` (String, Sensitive) Client secret from Azure + +Optional: + +- `autosync_teams_and_users` (Boolean) Set to true to sync user accounts in Azure AD to your Codefresh account +- `object_id` (String) The Object ID from your Enterprise Application Properties in Azure AD +- `sync_interval` (Number) Sync interval in hours for syncing user accounts in Azure AD to your Codefresh account. If not set the sync inteval will be 12 hours +- `tenant` (String) Azure tenant + + + +### Nested Schema for `github` + +Required: + +- `client_id` (String) Client ID from Github +- `client_secret` (String, Sensitive) Client secret from GitHub + +Optional: + +- `api_host` (String) GitHub API host, Defaults to api.github.com +- `api_path_prefix` (String) GitHub API url path prefix, defaults to / +- `authentication_url` (String) Authentication url, Defaults to https://github.com/login/oauth/authorize +- `token_url` (String) GitHub token endpoint url, Defaults to https://github.com/login/oauth/access_token +- `user_profile_url` (String) GitHub user profile url, Defaults to https://api.github.com/user + + + +### Nested Schema for `gitlab` + +Required: + +- `client_id` (String) Client ID from Gitlab +- `client_secret` (String, Sensitive) Client secret from Gitlab + +Optional: + +- `api_url` (String) Base url for Gitlab API, Defaults to https://gitlab.com/api/v4/ +- `authentication_url` (String) Authentication url, Defaults to https://gitlab.com +- `user_profile_url` (String) User profile url, Defaults to https://gitlab.com/api/v4/user + + + +### Nested Schema for `google` + +Required: + +- `client_id` (String) Client ID in Google, must be unique across all identity providers in Codefresh +- `client_secret` (String, Sensitive) Client secret in Google + +Optional: + +- `admin_email` (String) Email of a user with admin permissions on google, relevant only for synchronization +- `allowed_groups_for_sync` (String) Comma separated list of groups to sync +- `json_keyfile` (String) JSON keyfile for google service account used for synchronization +- `sync_field` (String) Relevant for custom schema-based synchronization only. See Codefresh documentation + + + +### Nested Schema for `keycloak` + +Required: + +- `client_id` (String) Client ID from Keycloak +- `client_secret` (String, Sensitive) Client secret from Keycloak +- `host` (String) The Keycloak URL + +Optional: + +- `realm` (String) The Realm ID for Codefresh in Keycloak. Defaults to master + + + +### Nested Schema for `ldap` + +Required: + +- `password` (String, Sensitive) The password of the user defined in Distinguished name that will be used to search other users +- `search_base` (String) The search-user scope in LDAP notation +- `url` (String) ldap server url + +Optional: + +- `allowed_groups_for_sync` (String) To sync only by specified groups - specify a comma separated list of groups, by default all groups will be synced +- `certificate` (String) For ldaps only: The security certificate of the LDAP server. Do not convert to base64 or any other encoding +- `distinguished_name` (String) The username to be used to search other users in LDAP notation (combination of cn, ou,dc) +- `search_base_for_sync` (String) Synchronize using a custom search base, by deafult seach_base is used +- `search_filter` (String) The attribute by which to search for the user on the LDAP server. By default, set to uid. For the Azure LDAP server, set this field to sAMAccountName + + + +### Nested Schema for `okta` + +Required: + +- `client_host` (String) The OKTA organization URL, for example, https://.okta.com +- `client_id` (String) Client ID in Okta, must be unique across all identity providers in Codefresh +- `client_secret` (String, Sensitive) Client secret in Okta + +Optional: + +- `access_token` (String) The Okta API token generated in Okta, used to sync groups and their users from Okta to Codefresh +- `app_id` (String) The Codefresh application ID in your OKTA organization +- `sync_mirror_accounts` (List of String) The names of the additional Codefresh accounts to be synced from Okta + + + +### Nested Schema for `onelogin` + +Required: + +- `client_id` (String) Client ID from Onelogin +- `client_secret` (String, Sensitive) Client secret from Onelogin +- `domain` (String) The domain to be used for authentication + +Optional: + +- `api_client_id` (String) Client ID for onelogin API, only needed if syncing users and groups from Onelogin +- `api_client_secret` (String) Client secret for onelogin API, only needed if syncing users and groups from Onelogin +- `app_id` (String) The Codefresh application ID in your Onelogin + + + +### Nested Schema for `saml` + +Required: + +- `application_certificate` (String, Sensitive) The security certificate of your Identity Provider. Paste the value directly on the field. Do not convert to base64 or any other encoding by hand +- `endpoint` (String) The SSO endpoint of your Identity Provider + +Optional: + +- `access_token` (String) Valid for Okta only: The Okta API token generated in Okta, used to sync groups and their users from Okta to Codefresh +- `activate_users_after_sync` (Boolean) Valid for Okta only: If set to true, Codefresh will automatically invite and activate new users added during the automated sync, without waiting for the users to accept the invitations. Defaults to false +- `admin_email` (String) Valid for GSuite only: Email of a user with admin permissions on google, relevant only for synchronization +- `allowed_groups_for_sync` (String) Valid for GSuite only: Comma separated list of groups to sync +- `app_id` (String) Valid for Okta only: The Codefresh application ID in Okta +- `autosync_teams_and_users` (Boolean) Valid for Okta/GSuite: Set to true to sync user accounts and teams in okta/gsuite to your Codefresh account +- `client_host` (String) Valid for Okta only: OKTA organization URL, for example, https://.okta.com +- `json_keyfile` (String) Valid for GSuite only: JSON keyfile for google service account used for synchronization +- `provider` (String) SAML provider. Currently supported values - GSuite, okta or empty string for generic provider. Defaults to empty string +- `sync_interval` (Number) Valid for Okta/GSuite: Sync interval in hours for syncing user accounts in okta/gsuite to your Codefresh account. If not set the sync inteval will be 12 hours + +## Import + +Please note that secret fields are not imported. +
All secrets should be provided in the configuration and applied after the import for the state to be consistent. + +```sh +terraform import codefresh_account_idp.test +``` diff --git a/templates/guides/development.md.tmpl b/templates/guides/development.md.tmpl index d1369fa..07d0051 100644 --- a/templates/guides/development.md.tmpl +++ b/templates/guides/development.md.tmpl @@ -21,7 +21,7 @@ Set the [developer overrides](https://developer.hashicorp.com/terraform/cli/conf # `~/.terraformrc (Windows: %APPDATA%/.terraformrc) provider_installation { dev_overrides { - "codefresh.io/codefresh" = "[REPLACE WITH GOPATH]/bin" + "codefresh-io/codefresh" = "[REPLACE WITH GOPATH]/bin" } direct {} } diff --git a/templates/resources/account_idp.md.tmpl b/templates/resources/account_idp.md.tmpl new file mode 100644 index 0000000..9807796 --- /dev/null +++ b/templates/resources/account_idp.md.tmpl @@ -0,0 +1,57 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example usage +```hcl +resource "codefresh_account_idp" "auth0-test" { + display_name = "tf-auth0-example" + + auth0 { + client_id = "auht0-codefresh-example" + client_secret = "mysecret" + domain = "codefresh.auth0.com" + } +} +``` +```hcl +resource "codefresh_account_idp" "google-example" { + display_name = "tf-google-example" + + google { + client_id = "google-codefresh-example" + client_secret = "mysecret99" + admin_email = "admin@codefresh.io" + sync_field = "myfield" + json_keyfile = <All secrets should be provided in the configuration and applied after the import for the state to be consistent. + +```sh +terraform import codefresh_account_idp.test +``` diff --git a/templates/resources/idp.md.tmpl b/templates/resources/idp.md.tmpl new file mode 100644 index 0000000..714f53d --- /dev/null +++ b/templates/resources/idp.md.tmpl @@ -0,0 +1,57 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example usage +```hcl +resource "codefresh_idp" "auth0-test" { + display_name = "tf-auth0-example" + + auth0 { + client_id = "auht0-codefresh-example" + client_secret = "mysecret" + domain = "codefresh.auth0.com" + } +} +``` +```hcl +resource "codefresh_idp" "google-example" { + display_name = "tf-google-example" + + google { + client_id = "google-codefresh-example" + client_secret = "mysecret99" + admin_email = "admin@codefresh.io" + sync_field = "myfield" + json_keyfile = <All secrets should be provided in the configuration and applied after the import for the state to be consistent. + +```sh +terraform import codefresh_account_idp.test +```