diff --git a/gitlab/provider.go b/gitlab/provider.go index f1b33eb67..2277d5c14 100644 --- a/gitlab/provider.go +++ b/gitlab/provider.go @@ -84,6 +84,7 @@ func Provider() terraform.ResourceProvider { "gitlab_service_jira": resourceGitlabServiceJira(), "gitlab_project_share_group": resourceGitlabProjectShareGroup(), "gitlab_group_cluster": resourceGitlabGroupCluster(), + "gitlab_group_ldap_link": resourceGitlabGroupLdapLink(), }, ConfigureFunc: providerConfigure, diff --git a/gitlab/resource_gitlab_group_ldap_link.go b/gitlab/resource_gitlab_group_ldap_link.go new file mode 100644 index 000000000..92e9b6c9e --- /dev/null +++ b/gitlab/resource_gitlab_group_ldap_link.go @@ -0,0 +1,156 @@ +package gitlab + +import ( + "errors" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + gitlab "github.com/xanzy/go-gitlab" +) + +func resourceGitlabGroupLdapLink() *schema.Resource { + acceptedAccessLevels := make([]string, 0, len(accessLevelID)) + for k := range accessLevelID { + acceptedAccessLevels = append(acceptedAccessLevels, k) + } + return &schema.Resource{ + Create: resourceGitlabGroupLdapLinkCreate, + Read: resourceGitlabGroupLdapLinkRead, + Delete: resourceGitlabGroupLdapLinkDelete, + + Schema: map[string]*schema.Schema{ + "group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + // Using the friendlier "access_level" here instead of the GitLab API "group_access". + "access_level": { + Type: schema.TypeString, + ValidateFunc: validateValueFunc(acceptedAccessLevels), + Required: true, + ForceNew: true, + }, + // Changing GitLab API parameter "provider" to "ldap_provider" to avoid clashing with the Terraform "provider" key word + "ldap_provider": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "force": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + }, + } +} + +func resourceGitlabGroupLdapLinkCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*gitlab.Client) + + groupId := d.Get("group_id").(string) + cn := d.Get("cn").(string) + group_access := int(accessLevelNameToValue[d.Get("access_level").(string)]) + ldap_provider := d.Get("ldap_provider").(string) + force := d.Get("force").(bool) + + options := &gitlab.AddGroupLDAPLinkOptions{ + CN: &cn, + GroupAccess: &group_access, + Provider: &ldap_provider, + } + + if force { + resourceGitlabGroupLdapLinkDelete(d, meta) + } + + log.Printf("[DEBUG] Create GitLab group LdapLink %s", d.Id()) + LdapLink, _, err := client.Groups.AddGroupLDAPLink(groupId, options) + if err != nil { + return err + } + + d.SetId(buildTwoPartID(&LdapLink.Provider, &LdapLink.CN)) + + return resourceGitlabGroupLdapLinkRead(d, meta) +} + +func resourceGitlabGroupLdapLinkRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*gitlab.Client) + groupId := d.Get("group_id").(string) + + // Try to fetch all group links from GitLab + log.Printf("[DEBUG] Read GitLab group LdapLinks %s", groupId) + ldapLinks, _, err := client.Groups.ListGroupLDAPLinks(groupId, nil) + if err != nil { + // The read/GET API wasn't implemented in GitLab until version 12.8 (March 2020, well after the add and delete APIs). + // If we 404, assume GitLab is at an older version and take things on faith. + switch err.(type) { + case *gitlab.ErrorResponse: + if err.(*gitlab.ErrorResponse).Response.StatusCode == 404 { + log.Printf("[WARNING] This GitLab instance doesn't have the GET API for group_ldap_sync. Please upgrade to 12.8 or later for best results.") + } else { + return err + } + default: + return err + } + } + + // If we got here and don't have links, assume GitLab is below version 12.8 and skip the check + if ldapLinks != nil { + // Check if the LDAP link exists in the returned list of links + found := false + for _, ldapLink := range ldapLinks { + if buildTwoPartID(&ldapLink.Provider, &ldapLink.CN) == d.Id() { + d.Set("group_id", groupId) + d.Set("cn", ldapLink.CN) + d.Set("group_access", ldapLink.GroupAccess) + d.Set("ldap_provider", ldapLink.Provider) + found = true + break + } + } + + if !found { + d.SetId("") + return errors.New(fmt.Sprintf("LdapLink %s does not exist.", d.Id())) + } + } + + return nil +} + +func resourceGitlabGroupLdapLinkDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*gitlab.Client) + groupId := d.Get("group_id").(string) + cn := d.Get("cn").(string) + ldap_provider := d.Get("ldap_provider").(string) + + log.Printf("[DEBUG] Delete GitLab group LdapLink %s", d.Id()) + _, err := client.Groups.DeleteGroupLDAPLinkForProvider(groupId, ldap_provider, cn) + if err != nil { + switch err.(type) { + case *gitlab.ErrorResponse: + // Ignore LDAP links that don't exist + if strings.Contains(string(err.(*gitlab.ErrorResponse).Message), "Linked LDAP group not found") { + log.Printf("[WARNING] %s", err) + } else { + return err + } + default: + return err + } + } + + return nil +} diff --git a/gitlab/resource_gitlab_group_ldap_link_test.go b/gitlab/resource_gitlab_group_ldap_link_test.go new file mode 100644 index 000000000..41facf08e --- /dev/null +++ b/gitlab/resource_gitlab_group_ldap_link_test.go @@ -0,0 +1,259 @@ +package gitlab + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/xanzy/go-gitlab" +) + +func TestAccGitlabGroupLdapLink_basic(t *testing.T) { + var testLdapLink gitlab.LDAPGroupLink + var ldapLink gitlab.LDAPGroupLink + rInt := acctest.RandInt() + testDataFile := "testdata/resource_gitlab_group_ldap_link.json" + + // PreCheck runs after Config so load test data here + err := testAccLoadTestData(testDataFile, &testLdapLink) + if err != nil { + t.Fatalf("[ERROR] Failed to load test data: %s", err.Error()) + } + + if testLdapLink.CN == "default" || testLdapLink.Provider == "default" { + t.Skipf("[WARNING] Skipping test until test data is configured in %s.", testDataFile) + } else { + resource.Test(t, resource.TestCase{ + //PreCheck: func() {testAccPreCheck(t)}, + Providers: testAccProviders, + CheckDestroy: testAccCheckGitlabGroupLdapLinkDestroy, + Steps: []resource.TestStep{ + + // Create a group LDAP link as a developer (uses testAccGitlabGroupLdapLinkCreateConfig for Config) + { + Config: testAccGitlabGroupLdapLinkCreateConfig(rInt, &testLdapLink), + Check: resource.ComposeTestCheckFunc( + testAccCheckGitlabGroupLdapLinkExists("gitlab_group_ldap_link.foo", &ldapLink), + testAccCheckGitlabGroupLdapLinkAttributes(&ldapLink, &testAccGitlabGroupLdapLinkExpectedAttributes{ + accessLevel: fmt.Sprintf("developer"), + })), + }, + + // Update the group LDAP link to change the access level (uses testAccGitlabGroupLdapLinkUpdateConfig for Config) + { + Config: testAccGitlabGroupLdapLinkUpdateConfig(rInt, &testLdapLink), + Check: resource.ComposeTestCheckFunc( + testAccCheckGitlabGroupLdapLinkExists("gitlab_group_ldap_link.foo", &ldapLink), + testAccCheckGitlabGroupLdapLinkAttributes(&ldapLink, &testAccGitlabGroupLdapLinkExpectedAttributes{ + accessLevel: fmt.Sprintf("maintainer"), + })), + }, + + // Force create the same group LDAP link in a different resource (uses testAccGitlabGroupLdapLinkForceCreateConfig for Config) + { + Config: testAccGitlabGroupLdapLinkForceCreateConfig(rInt, &testLdapLink), + Check: resource.ComposeTestCheckFunc( + testAccCheckGitlabGroupLdapLinkExists("gitlab_group_ldap_link.bar", &ldapLink), + testAccCheckGitlabGroupLdapLinkAttributes(&ldapLink, &testAccGitlabGroupLdapLinkExpectedAttributes{ + accessLevel: fmt.Sprintf("developer"), + })), + }, + }, + }) + } +} + +func testAccCheckGitlabGroupLdapLinkExists(resourceName string, ldapLink *gitlab.LDAPGroupLink) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Clear the "found" LDAP link before checking for existence + *ldapLink = gitlab.LDAPGroupLink{} + + resourceState, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + err := testAccGetGitlabGroupLdapLink(ldapLink, resourceState) + if err != nil { + return err + } + + return nil + } +} + +type testAccGitlabGroupLdapLinkExpectedAttributes struct { + accessLevel string +} + +func testAccCheckGitlabGroupLdapLinkAttributes(ldapLink *gitlab.LDAPGroupLink, want *testAccGitlabGroupLdapLinkExpectedAttributes) resource.TestCheckFunc { + return func(s *terraform.State) error { + + accessLevelId, ok := accessLevel[ldapLink.GroupAccess] + if !ok { + return fmt.Errorf("Invalid access level '%s'", accessLevelId) + } + if accessLevelId != want.accessLevel { + return fmt.Errorf("Has access level %s; want %s", accessLevelId, want.accessLevel) + } + return nil + } +} + +func testAccCheckGitlabGroupLdapLinkDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*gitlab.Client) + + // Can't check for links if the group is destroyed so make sure all groups are destroyed instead + for _, resourceState := range s.RootModule().Resources { + if resourceState.Type != "gitlab_group" { + continue + } + + group, resp, err := conn.Groups.GetGroup(resourceState.Primary.ID) + if err == nil { + if group != nil && fmt.Sprintf("%d", group.ID) == resourceState.Primary.ID { + if group.MarkedForDeletionOn == nil { + return fmt.Errorf("Group still exists") + } + } + } + if resp.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +func testAccGetGitlabGroupLdapLink(ldapLink *gitlab.LDAPGroupLink, resourceState *terraform.ResourceState) error { + conn := testAccProvider.Meta().(*gitlab.Client) + + groupId := resourceState.Primary.Attributes["group_id"] + if groupId == "" { + return fmt.Errorf("No group ID is set") + } + + // Construct our desired LDAP Link from the config values + desiredLdapLink := gitlab.LDAPGroupLink{ + CN: resourceState.Primary.Attributes["cn"], + GroupAccess: accessLevelNameToValue[resourceState.Primary.Attributes["access_level"]], + Provider: resourceState.Primary.Attributes["ldap_provider"], + } + + desiredLdapLinkId := buildTwoPartID(&desiredLdapLink.Provider, &desiredLdapLink.CN) + + // Try to fetch all group links from GitLab + currentLdapLinks, _, err := conn.Groups.ListGroupLDAPLinks(groupId, nil) + if err != nil { + // The read/GET API wasn't implemented in GitLab until version 12.8 (March 2020, well after the add and delete APIs). + // If we 404, assume GitLab is at an older version and take things on faith. + switch err.(type) { + case *gitlab.ErrorResponse: + if err.(*gitlab.ErrorResponse).Response.StatusCode == 404 { + // Do nothing + } else { + return err + } + default: + return err + } + } + + // If we got here and don't have links, assume GitLab is below version 12.8 and skip the check + if currentLdapLinks != nil { + found := false + + // Check if the LDAP link exists in the returned list of links + for _, currentLdapLink := range currentLdapLinks { + if buildTwoPartID(¤tLdapLink.Provider, ¤tLdapLink.CN) == desiredLdapLinkId { + found = true + *ldapLink = *currentLdapLink + break + } + } + + if !found { + return errors.New(fmt.Sprintf("LdapLink %s does not exist.", desiredLdapLinkId)) + } + } else { + *ldapLink = desiredLdapLink + } + + return nil +} + +func testAccLoadTestData(testdatafile string, ldapLink *gitlab.LDAPGroupLink) error { + testLdapLinkBytes, err := ioutil.ReadFile(testdatafile) + if err != nil { + return err + } + + err = json.Unmarshal(testLdapLinkBytes, ldapLink) + if err != nil { + return err + } + + return nil +} + +func testAccGitlabGroupLdapLinkCreateConfig(rInt int, testLdapLink *gitlab.LDAPGroupLink) string { + return fmt.Sprintf(` +resource "gitlab_group" "foo" { + name = "foo%d" + path = "foo%d" + description = "Terraform acceptance test - Group LDAP Links 1" +} + +resource "gitlab_group_ldap_link" "foo" { + group_id = "${gitlab_group.foo.id}" + cn = "%s" + access_level = "developer" + ldap_provider = "%s" + +}`, rInt, rInt, testLdapLink.CN, testLdapLink.Provider) +} + +func testAccGitlabGroupLdapLinkUpdateConfig(rInt int, testLdapLink *gitlab.LDAPGroupLink) string { + return fmt.Sprintf(` +resource "gitlab_group" "foo" { + name = "foo%d" + path = "foo%d" + description = "Terraform acceptance test - Group LDAP Links 2" +} + +resource "gitlab_group_ldap_link" "foo" { + group_id = "${gitlab_group.foo.id}" + cn = "%s" + access_level = "maintainer" + ldap_provider = "%s" +}`, rInt, rInt, testLdapLink.CN, testLdapLink.Provider) +} + +func testAccGitlabGroupLdapLinkForceCreateConfig(rInt int, testLdapLink *gitlab.LDAPGroupLink) string { + return fmt.Sprintf(` +resource "gitlab_group" "foo" { + name = "foo%d" + path = "foo%d" + description = "Terraform acceptance test - Group LDAP Links 3" +} + +resource "gitlab_group_ldap_link" "foo" { + group_id = "${gitlab_group.foo.id}" + cn = "%s" + access_level = "maintainer" + ldap_provider = "%s" +} + +resource "gitlab_group_ldap_link" "bar" { + group_id = "${gitlab_group.foo.id}" + cn = "%s" + access_level = "developer" + ldap_provider = "%s" + force = true +}`, rInt, rInt, testLdapLink.CN, testLdapLink.Provider, testLdapLink.CN, testLdapLink.Provider) +} diff --git a/gitlab/testdata/resource_gitlab_group_ldap_link.json b/gitlab/testdata/resource_gitlab_group_ldap_link.json new file mode 100644 index 000000000..8f7d69b9c --- /dev/null +++ b/gitlab/testdata/resource_gitlab_group_ldap_link.json @@ -0,0 +1,4 @@ +{ + "cn": "default", + "provider": "default" +} \ No newline at end of file diff --git a/website/docs/r/group_ldap_link.markdown b/website/docs/r/group_ldap_link.markdown new file mode 100644 index 000000000..e64b85cc4 --- /dev/null +++ b/website/docs/r/group_ldap_link.markdown @@ -0,0 +1,42 @@ +--- +layout: "gitlab" +page_title: "GitLab: gitlab_group_ldap_link" +sidebar_current: "docs-gitlab-resource-group_ldap_link" +description: |- + Adds an LDAP link to an existing GitLab group. +--- + +# gitlab\_group_ldap_link + +This resource allows you to add an LDAP link to an existing GitLab group. + +## Example Usage + +```hcl +resource "gitlab_group_ldap_link" "test" { + group_id = "12345" + cn = "testuser" + access_level = "developer" + ldap_provider = "ldapmain" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `group_id` - (Required) The id of the GitLab group. + +* `cn` - (Required) The CN of the LDAP group to link with. + +* `access_level` - (Required) Acceptable values are: guest, reporter, developer, maintainer, owner. + +* `ldap_provider` - (Required) The name of the LDAP provider as stored in the GitLab database. + +## Import + +GitLab group ldap links can be imported using an id made up of `ldap_provider:cn`, e.g. + +``` +$ terraform import gitlab_group_ldap_link.test "ldapmain:testuser" +```