Skip to content

Commit 0492479

Browse files
mhodgsontimofurrer
authored andcommitted
Add SAML Group link resource
1 parent e37d0f2 commit 0492479

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed

internal/provider/access_level_helpers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ var validProjectEnvironmentStates = []string{
6565
"available", "stopped",
6666
}
6767

68+
var validGroupSamlLinkAccessLevelNames = []string{
69+
"Guest",
70+
"Reporter",
71+
"Developer",
72+
"Maintainer",
73+
"Owner"
74+
}
75+
6876
var accessLevelNameToValue = map[string]gitlab.AccessLevelValue{
6977
"no one": gitlab.NoPermissions,
7078
"minimal": gitlab.MinimalAccessPermissions,
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"strings"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
12+
gitlab "github.com/xanzy/go-gitlab"
13+
)
14+
15+
var _ = registerResource("gitlab_group_saml_link", func() *schema.Resource {
16+
return &schema.Resource{
17+
Description: `The ` + "`gitlab_group_saml_link`" + ` resource allows to manage the lifecycle of an SAML integration with a group.
18+
19+
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/groups.html#saml-group-links)`,
20+
21+
CreateContext: resourceGitlabGroupSamlLinkCreate,
22+
ReadContext: resourceGitlabGroupSamlLinkRead,
23+
DeleteContext: resourceGitlabGroupSamlLinkDelete,
24+
Importer: &schema.ResourceImporter{
25+
StateContext: resourceGitlabGroupSamlLinkImporter,
26+
},
27+
28+
Schema: map[string]*schema.Schema{
29+
"group_id": {
30+
Description: "The id of the GitLab group.",
31+
Type: schema.TypeString,
32+
Required: true,
33+
ForceNew: true,
34+
},
35+
"access_level": {
36+
Description: fmt.Sprintf("Minimum access level for members of the SAML group. Valid values are: %s", renderValueListForDocs(validGroupSamlLinkAccessLevelNames)),
37+
Type: schema.TypeString,
38+
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(validGroupSamlLinkAccessLevelNames, false)),
39+
Required: true,
40+
ForceNew: true,
41+
},
42+
"saml_group_name": {
43+
Description: "The name of the SAML group.",
44+
Type: schema.TypeString,
45+
Required: true,
46+
ForceNew: true,
47+
},
48+
"force": {
49+
Description: "If true, then delete and replace an existing SAML link if one exists.",
50+
Type: schema.TypeBool,
51+
Optional: true,
52+
Default: false,
53+
ForceNew: true,
54+
},
55+
},
56+
}
57+
})
58+
59+
func resourceGitlabGroupSamlLinkCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
60+
client := meta.(*gitlab.Client)
61+
62+
groupId := d.Get("group_id").(string)
63+
accessLevel := d.Get("access_level").(string)
64+
samlGroupName := d.Get("saml_group_name").(string)
65+
force := d.Get("force").(bool)
66+
67+
options := &gitlab.AddGroupSAMLLinkOptions{
68+
AccessLevel: &accessLevel,
69+
SamlGroupName: &samlGroupName,
70+
}
71+
72+
if force {
73+
if err := resourceGitlabGroupSamlLinkDelete(ctx, d, meta); err != nil {
74+
return err
75+
}
76+
}
77+
78+
log.Printf("[DEBUG] Create GitLab group SamlLink %s", d.Id())
79+
SamlLink, _, err := client.Groups.AddGroupSAMLLink(groupId, options, gitlab.WithContext(ctx))
80+
if err != nil {
81+
return diag.FromErr(err)
82+
}
83+
84+
d.SetId(buildTwoPartID(&groupId, &SamlLink.Name))
85+
86+
return resourceGitlabGroupSamlLinkRead(ctx, d, meta)
87+
}
88+
89+
func resourceGitlabGroupSamlLinkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
90+
client := meta.(*gitlab.Client)
91+
groupId := d.Get("group_id").(string)
92+
93+
// Try to fetch all group links from GitLab
94+
log.Printf("[DEBUG] Read GitLab group SamlLinks %s", groupId)
95+
samlLinks, _, err := client.Groups.ListGroupSAMLLinks(groupId, nil, gitlab.WithContext(ctx))
96+
if err != nil {
97+
return diag.FromErr(err)
98+
}
99+
100+
// If we got here and don't have links, assume GitLab is below version 12.8 and skip the check
101+
if samlLinks != nil {
102+
// Check if the LDAP link exists in the returned list of links
103+
found := false
104+
for _, samlLink := range samlLinks {
105+
if buildTwoPartID(&groupId, &samlLink.Name) == d.Id() {
106+
d.Set("group_id", groupId)
107+
d.Set("access_level", samlLink.AccessLevel])
108+
d.Set("saml_group_name", samlLink.Name)
109+
found = true
110+
break
111+
}
112+
}
113+
114+
if !found {
115+
d.SetId("")
116+
return diag.Errorf("SamlLink %s does not exist.", d.Id())
117+
}
118+
}
119+
120+
return nil
121+
}
122+
123+
func resourceGitlabGroupSamlLinkDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
124+
client := meta.(*gitlab.Client)
125+
groupId := d.Get("group_id").(string)
126+
samlGroupName := d.Get("saml_group_name").(string)
127+
128+
log.Printf("[DEBUG] Delete GitLab group SamlLink %s", d.Id())
129+
_, err := client.Groups.DeleteGroupSAMLLink(groupId, samlGroupName, cn, gitlab.WithContext(ctx))
130+
if err != nil {
131+
switch err.(type) { // nolint // TODO: Resolve this golangci-lint issue: S1034: assigning the result of this type assertion to a variable (switch err := err.(type)) could eliminate type assertions in switch cases (gosimple)
132+
case *gitlab.ErrorResponse:
133+
// Ignore SAML links that don't exist
134+
if strings.Contains(string(err.(*gitlab.ErrorResponse).Message), "Linked SAML group not found") { // nolint // TODO: Resolve this golangci-lint issue: S1034(related information): could eliminate this type assertion (gosimple)
135+
log.Printf("[WARNING] %s", err)
136+
} else {
137+
return diag.FromErr(err)
138+
}
139+
default:
140+
return diag.FromErr(err)
141+
}
142+
}
143+
144+
return nil
145+
}
146+
147+
func resourceGitlabGroupSamlLinkImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
148+
parts := strings.SplitN(d.Id(), ":", 2)
149+
if len(parts) != 2 {
150+
return nil, fmt.Errorf("invalid saml link import id (should be <group id>:<saml group name>): %s", d.Id())
151+
}
152+
153+
groupId, samlGroupName := parts[0], parts[1]
154+
d.SetId(buildTwoPartID(&groupId, &samlGroupName))
155+
d.Set("group_id", groupId)
156+
d.Set("force", false)
157+
158+
diag := resourceGitlabGroupSamlLinkRead(ctx, d, meta)
159+
if diag.HasError() {
160+
return nil, fmt.Errorf("%s", diag[0].Summary)
161+
}
162+
return []*schema.ResourceData{d}, nil
163+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//go:build acceptance
2+
// +build acceptance
3+
4+
package provider
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
14+
"github.com/xanzy/go-gitlab"
15+
)
16+
17+
func TestAccGitlabGroupSamlLink_basic(t *testing.T) {
18+
rInt := acctest.RandInt()
19+
resourceName := "gitlab_group_saml_link.foo"
20+
21+
// PreCheck runs after Config so load test data here
22+
var samlLink gitlab.SAMLGroupLink
23+
testSamlLink := gitlab.SAMLGroupLink{
24+
Name: "test_saml_group"
25+
}
26+
27+
resource.ParallelTest(t, resource.TestCase{
28+
ProviderFactories: providerFactories,
29+
CheckDestroy: testAccCheckGitlabGroupSamlLinkDestroy,
30+
Steps: []resource.TestStep{
31+
32+
// Create a group SAML link as a developer (uses testAccGitlabGroupLdapSamlCreateConfig for Config)
33+
{
34+
SkipFunc: isRunningInCE,
35+
Config: testAccGitlabGroupSamlLinkCreateConfig(rInt, &testSamlLink),
36+
Check: resource.ComposeTestCheckFunc(
37+
testAccCheckGitlabGroupSamlLinkExists(resourceName, &samlLink)),
38+
},
39+
40+
// Import the group SAML link (re-uses testAccGitlabGroupSamlLinkCreateConfig for Config)
41+
{
42+
SkipFunc: isRunningInCE,
43+
ResourceName: resourceName,
44+
ImportStateIdFunc: getGitlabGroupSamlLinkImportID(resourceName),
45+
ImportState: true,
46+
ImportStateVerify: true,
47+
},
48+
49+
// Update the group SAML link to change the access level (uses testAccGitlabGroupSamlLinkUpdateConfig for Config)
50+
{
51+
SkipFunc: isRunningInCE,
52+
Config: testAccGitlabGroupSamlLinkUpdateConfig(rInt, &testSamlLink),
53+
Check: resource.ComposeTestCheckFunc(
54+
testAccCheckGitlabGroupSamlLinkExists(resourceName, &samlLink))
55+
},
56+
},
57+
})
58+
}
59+
60+
func getGitlabGroupSamlLinkImportID(resourceName string) resource.ImportStateIdFunc {
61+
return func(s *terraform.State) (string, error) {
62+
rs, ok := s.RootModule().Resources[resourceName]
63+
if !ok {
64+
return "", fmt.Errorf("Not Found: %s", resourceName)
65+
}
66+
67+
groupID := rs.Primary.Attributes["group_id"]
68+
if groupID == "" {
69+
return "", fmt.Errorf("No group ID is set")
70+
}
71+
samlGroupName := rs.Primary.Attributes["saml_group_name"]
72+
if samlGroupName == "" {
73+
return "", fmt.Errorf("No SAML group name is set")
74+
}
75+
76+
return fmt.Sprintf("%s:%s", groupID, samlGroupName), nil
77+
}
78+
}
79+
80+
func testAccCheckGitlabGroupSamlLinkExists(resourceName string, samlLink *gitlab.SAMLGroupLink) resource.TestCheckFunc {
81+
return func(s *terraform.State) error {
82+
// Clear the "found" SAML link before checking for existence
83+
*samlLink = gitlab.SAMLGroupLink{}
84+
85+
resourceState, ok := s.RootModule().Resources[resourceName]
86+
if !ok {
87+
return fmt.Errorf("Not found: %s", resourceName)
88+
}
89+
90+
err := testAccGetGitlabGroupSamlLink(samlLink, resourceState)
91+
if err != nil {
92+
return err
93+
}
94+
95+
return nil
96+
}
97+
}
98+
99+
func testAccCheckGitlabGroupSamlLinkDestroy(s *terraform.State) error {
100+
// Can't check for links if the group is destroyed so make sure all groups are destroyed instead
101+
for _, resourceState := range s.RootModule().Resources {
102+
if resourceState.Type != "gitlab_group" {
103+
continue
104+
}
105+
106+
group, _, err := testGitlabClient.Groups.GetGroup(resourceState.Primary.ID, nil)
107+
if err == nil {
108+
if group != nil && fmt.Sprintf("%d", group.ID) == resourceState.Primary.ID {
109+
if group.MarkedForDeletionOn == nil {
110+
return fmt.Errorf("Group still exists")
111+
}
112+
}
113+
}
114+
if !is404(err) {
115+
return err
116+
}
117+
return nil
118+
}
119+
return nil
120+
}
121+
122+
func testAccGetGitlabGroupSamlLink(samlLink *gitlab.SAMLGroupLink, resourceState *terraform.ResourceState) error {
123+
groupId := resourceState.Primary.Attributes["group_id"]
124+
if groupId == "" {
125+
return fmt.Errorf("No group ID is set")
126+
}
127+
128+
// Construct our desired SAML Link from the config values
129+
desiredSamlLink := gitlab.SAMLGroupLink{
130+
AccessLevel: resourceState.Primary.Attributes["access_level"],
131+
Name: resourceState.Primary.Attributes["saml_group_name"],
132+
}
133+
134+
desiredSamlLinkId := buildTwoPartID(&groupId, &desiredSamlLink.Name)
135+
136+
// Try to fetch all group links from GitLab
137+
currentSamlLinks, _, err := testGitlabClient.Groups.ListGroupSamlLinks(groupId, nil)
138+
if err != nil {
139+
return err
140+
}
141+
142+
found := false
143+
144+
// Check if the SAML link exists in the returned list of links
145+
for _, currentSamlLink := range currentSamlLinks {
146+
if buildTwoPartID(&groupId, &currentSamlLink.Name) == desiredSamlLinkId {
147+
found = true
148+
*samlLink = *currentSamlLink
149+
break
150+
}
151+
}
152+
153+
if !found {
154+
return errors.New(fmt.Sprintf("SamlLink %s does not exist.", desiredSamlLinkId)) // nolint // TODO: Resolve this golangci-lint issue: S1028: should use fmt.Errorf(...) instead of errors.New(fmt.Sprintf(...)) (gosimple)
155+
}
156+
157+
return nil
158+
}
159+
160+
func testAccGitlabGroupSamlLinkCreateConfig(rInt int, testSamlLink *gitlab.SAMLGroupLink) string {
161+
return fmt.Sprintf(`
162+
resource "gitlab_group" "foo" {
163+
name = "foo%d"
164+
path = "foo%d"
165+
description = "Terraform acceptance test - Group SAML Links 1"
166+
}
167+
168+
resource "gitlab_group_saml_link" "foo" {
169+
group_id = "${gitlab_group.foo.id}"
170+
access_level = "Developer"
171+
saml_group_name = "%s"
172+
173+
}`, rInt, rInt, testSamlLink.Name)
174+
}
175+
176+
func testAccGitlabGroupSamlLinkUpdateConfig(rInt int, testSamlLink *gitlab.SAMLGroupLink) string {
177+
return fmt.Sprintf(`
178+
resource "gitlab_group" "foo" {
179+
name = "foo%d"
180+
path = "foo%d"
181+
description = "Terraform acceptance test - Group SAML Links 2"
182+
}
183+
184+
resource "gitlab_group_saml_link" "foo" {
185+
group_id = "${gitlab_group.foo.id}"
186+
access_level = "Maintainer"
187+
saml_group_name = "%s"
188+
}`, rInt, rInt, testSamlLink.Name)
189+
}

0 commit comments

Comments
 (0)