diff --git a/docs/resources/group_saml_link.md b/docs/resources/group_saml_link.md new file mode 100644 index 000000000..3b681b561 --- /dev/null +++ b/docs/resources/group_saml_link.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitlab_group_saml_link Resource - terraform-provider-gitlab" +subcategory: "" +description: |- + The gitlab_group_saml_link resource allows to manage the lifecycle of an SAML integration with a group. + Upstream API: GitLab REST API docs https://docs.gitlab.com/ee/api/groups.html#saml-group-links +--- + +# gitlab_group_saml_link (Resource) + +The `gitlab_group_saml_link` resource allows to manage the lifecycle of an SAML integration with a group. + +**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/groups.html#saml-group-links) + +## Example Usage + +```terraform +resource "gitlab_group_saml_link" "test" { + group_id = "12345" + access_level = "Developer" + saml_group_name = "samlgroupname1" +} +``` + + +## Schema + +### Required + +- `access_level` (String) Access level for members of the SAML group. Valid values are: `guest`, `reporter`, `developer`, `maintainer`, `owner`. +- `group` (String) The ID or path of the group to add the SAML Group Link to. +- `saml_group_name` (String) The name of the SAML group. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +# GitLab group saml links can be imported using an id made up of `group_id:saml_group_name`, e.g. +terraform import gitlab_group_saml_link.test "12345:samlgroupname1" +``` diff --git a/examples/resources/gitlab_group_saml_link/import.sh b/examples/resources/gitlab_group_saml_link/import.sh new file mode 100644 index 000000000..46f957744 --- /dev/null +++ b/examples/resources/gitlab_group_saml_link/import.sh @@ -0,0 +1,2 @@ +# GitLab group saml links can be imported using an id made up of `group_id:saml_group_name`, e.g. +terraform import gitlab_group_saml_link.test "12345:samlgroupname1" diff --git a/examples/resources/gitlab_group_saml_link/resource.tf b/examples/resources/gitlab_group_saml_link/resource.tf new file mode 100644 index 000000000..8a9f87977 --- /dev/null +++ b/examples/resources/gitlab_group_saml_link/resource.tf @@ -0,0 +1,5 @@ +resource "gitlab_group_saml_link" "test" { + group_id = "12345" + access_level = "Developer" + saml_group_name = "samlgroupname1" +} diff --git a/internal/provider/resource_gitlab_group_saml_link.go b/internal/provider/resource_gitlab_group_saml_link.go new file mode 100644 index 000000000..12d1b6fcd --- /dev/null +++ b/internal/provider/resource_gitlab_group_saml_link.go @@ -0,0 +1,125 @@ +package provider + +import ( + "context" + "fmt" + "log" + + "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" + gitlab "github.com/xanzy/go-gitlab" +) + +var _ = registerResource("gitlab_group_saml_link", func() *schema.Resource { + validGroupSamlLinkAccessLevelNames := []string{ + "guest", + "reporter", + "developer", + "maintainer", + "owner", + } + + return &schema.Resource{ + Description: `The ` + "`gitlab_group_saml_link`" + ` resource allows to manage the lifecycle of an SAML integration with a group. + +**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/groups.html#saml-group-links)`, + + CreateContext: resourceGitlabGroupSamlLinkCreate, + ReadContext: resourceGitlabGroupSamlLinkRead, + DeleteContext: resourceGitlabGroupSamlLinkDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "group": { + Description: "The ID or path of the group to add the SAML Group Link to.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "saml_group_name": { + Description: "The name of the SAML group.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "access_level": { + Description: fmt.Sprintf("Access level for members of the SAML group. Valid values are: %s.", renderValueListForDocs(validGroupSamlLinkAccessLevelNames)), + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(validGroupSamlLinkAccessLevelNames, false)), + Required: true, + ForceNew: true, + }, + }, + } +}) + +func resourceGitlabGroupSamlLinkCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*gitlab.Client) + + group := d.Get("group").(string) + samlGroupName := d.Get("saml_group_name").(string) + accessLevel := accessLevelNameToValue[d.Get("access_level").(string)] + + options := &gitlab.AddGroupSAMLLinkOptions{ + SAMLGroupName: gitlab.String(samlGroupName), + AccessLevel: gitlab.AccessLevel(accessLevel), + } + + log.Printf("[DEBUG] Create GitLab Group SAML Link for group %q with name %q", group, samlGroupName) + SamlLink, _, err := client.Groups.AddGroupSAMLLink(group, options, gitlab.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildTwoPartID(&group, &SamlLink.Name)) + return resourceGitlabGroupSamlLinkRead(ctx, d, meta) +} + +func resourceGitlabGroupSamlLinkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*gitlab.Client) + group, samlGroupName, parse_err := parseTwoPartID(d.Id()) + if parse_err != nil { + return diag.FromErr(parse_err) + } + + // Try to fetch all group links from GitLab + log.Printf("[DEBUG] Read GitLab Group SAML Link for group %q", group) + samlLink, _, err := client.Groups.GetGroupSAMLLink(group, samlGroupName, nil, gitlab.WithContext(ctx)) + if err != nil { + if is404(err) { + log.Printf("[DEBUG] GitLab SAML Group Link %s for group ID %s not found, removing from state", samlGroupName, group) + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + d.Set("group", group) + d.Set("access_level", accessLevelValueToName[samlLink.AccessLevel]) + d.Set("saml_group_name", samlLink.Name) + + return nil +} + +func resourceGitlabGroupSamlLinkDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*gitlab.Client) + group, samlGroupName, parse_err := parseTwoPartID(d.Id()) + if parse_err != nil { + return diag.FromErr(parse_err) + } + + log.Printf("[DEBUG] Delete GitLab Group SAML Link for group %q with name %q", group, samlGroupName) + _, err := client.Groups.DeleteGroupSAMLLink(group, samlGroupName, gitlab.WithContext(ctx)) + if err != nil { + if is404(err) { + log.Printf("[WARNING] %s", err) + } else { + return diag.FromErr(err) + } + } + + return nil +} diff --git a/internal/provider/resource_gitlab_group_saml_link_test.go b/internal/provider/resource_gitlab_group_saml_link_test.go new file mode 100644 index 000000000..0cd5486de --- /dev/null +++ b/internal/provider/resource_gitlab_group_saml_link_test.go @@ -0,0 +1,80 @@ +//go:build acceptance +// +build acceptance + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccGitlabGroupSamlLink_basic(t *testing.T) { + testAccCheckEE(t) + testAccRequiresAtLeast(t, "15.3") + + testGroup := testAccCreateGroups(t, 1)[0] + + resource.ParallelTest(t, resource.TestCase{ + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckGitlabGroupSamlLinkDestroy, + Steps: []resource.TestStep{ + + // Create a group SAML link as a developer + { + Config: fmt.Sprintf(` + resource "gitlab_group_saml_link" "this" { + group = "%d" + access_level = "developer" + saml_group_name = "test_saml_group" + + } + `, testGroup.ID), + }, + // Verify Import + { + ResourceName: "gitlab_group_saml_link.this", + ImportState: true, + ImportStateVerify: true, + }, + // Update the group SAML link to change the access level + { + Config: fmt.Sprintf(` + resource "gitlab_group_saml_link" "this" { + group = "%d" + access_level = "maintainer" + saml_group_name = "test_saml_group" + + } + `, testGroup.ID), + }, + }, + }) +} + +func testAccCheckGitlabGroupSamlLinkDestroy(s *terraform.State) error { + for _, resourceState := range s.RootModule().Resources { + if resourceState.Type != "gitlab_group_saml_link" { + continue + } + + group, samlGroupName, err := parseTwoPartID(resourceState.Primary.ID) + if err != nil { + return err + } + + samlGroupLink, _, err := testGitlabClient.Groups.GetGroupSAMLLink(group, samlGroupName) + if err == nil { + if samlGroupLink != nil { + return fmt.Errorf("SAML Group Link still exists") + } + } + if !is404(err) { + return err + } + return nil + } + return nil +}