Skip to content

Commit

Permalink
Merge pull request #158 from jfrog/GH-154-add-support-for-ldap-group-…
Browse files Browse the repository at this point in the history
…settings

Add support for ldap group settings
  • Loading branch information
alexhung authored Nov 20, 2024
2 parents 14a279b + bef7002 commit 03800ce
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 57 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.18.0 (November 21, 2024). Tested on Artifactory 7.98.8 with Terraform 1.9.8 and OpenTofu 1.8.5

IMPROVEMENTS:

* resource/platform_saml_settings: Add `ldap_group_settings` attribute to support LDAP groups synchronization. Issue: [#154](https://github.com/jfrog/terraform-provider-platform/issues/154) PR: [#158](https://github.com/jfrog/terraform-provider-platform/pull/158)

## 1.17.0 (November 15, 2024). Tested on Artifactory 7.98.8 with Terraform 1.9.8 and OpenTofu 1.8.5

FEATURES:
Expand Down
4 changes: 2 additions & 2 deletions docs/resources/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
page_title: "platform_group Resource - terraform-provider-platform"
subcategory: ""
description: |-
Provides a group resource. This can be used to create and manage groups. A group represents a role in the system and is assigned a set of permissions.
Provides a group resource to create and manage groups, and manages membership. A group represents a role and is used with RBAC (Role-Based Access Control) rules. See JFrog documentation https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-and-edit-groups for more details.
---

# platform_group (Resource)

Provides a group resource. This can be used to create and manage groups. A group represents a role in the system and is assigned a set of permissions.
Provides a group resource to create and manage groups, and manages membership. A group represents a role and is used with RBAC (Role-Based Access Control) rules. See [JFrog documentation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-and-edit-groups) for more details.

## Example Usage

Expand Down
1 change: 1 addition & 0 deletions docs/resources/saml_settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ resource "platform_saml_settings" "my-okta-saml-settings" {
- `email_attribute` (String) If `no_auto_user_creation` is diabled or an internal user exists, the system will set the user's email to the value in this attribute that is returned by the SAML login XML response.
- `enable` (Boolean) When set, SAML integration is enabled and users may be authenticated via a SAML server. Default value is `true`.
- `group_attribute` (String) The group attribute in the SAML login XML response. Note that the system will search for a case-sensitive match to an existing group..
- `ldap_group_settings` (Set of String) List of LDAP group setting names. Only support in Artifactory 7.98 or later. See [Enabling Synchronization of LDAP Groups for SAML SSO](https://jfrog.com/help/r/jfrog-platform-administration-documentation/enabling-synchronization-of-ldap-groups-for-saml-sso) for more details.
- `name_id_attribute` (String) The username attribute used to configure the SSO URL for the identity provider.
- `no_auto_user_creation` (Boolean, Deprecated) **Deprecated** Use `auto_user_creation` instead. When disabled, the system will automatically create new users for those who have logged in using SAML, and assign them to the default groups. Default value is `false`.
- `sync_groups` (Boolean) When set, in addition to the groups the user is already associated with, he will also be associated with the groups returned in the SAML login response. Note that the user's association with the returned groups is not persistent. It is only valid for the current login session. Default value is `false`.
Expand Down
120 changes: 66 additions & 54 deletions pkg/platform/resource_saml_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package platform

import (
"context"
"fmt"
"net/http"

"github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
Expand All @@ -22,20 +22,19 @@ import (
"github.com/samber/lo"
)

const (
SAMLSettingsEndpoint = "access/api/v1/saml"
SAMLSettingEndpoint = "access/api/v1/saml/{name}"
)

func NewSAMLSettingsResource() resource.Resource {
return &SAMLSettingsResource{
TypeName: "platform_saml_settings",
JFrogResource: util.JFrogResource{
TypeName: "platform_saml_settings",
ValidArtifactoryVersion: "7.83.1",
DocumentEndpoint: "access/api/v1/saml/{name}",
CollectionEndpoint: "access/api/v1/saml",
},
}
}

type SAMLSettingsResource struct {
ProviderData PlatformProviderMetadata
TypeName string
util.JFrogResource
}

type SAMLSettingsResourceModelV0 struct {
Expand All @@ -58,10 +57,12 @@ type SAMLSettingsResourceModelV0 struct {

type SAMLSettingsResourceModelV1 struct {
SAMLSettingsResourceModelV0
AutoUserCreation types.Bool `tfsdk:"auto_user_creation"`
AutoUserCreation types.Bool `tfsdk:"auto_user_creation"`
LDAPGroupSettings types.Set `tfsdk:"ldap_group_settings"`
}

func (r *SAMLSettingsResourceModelV1) toAPIModel(_ context.Context, apiModel *SAMLSettingsAPIModel) diag.Diagnostics {
func (r *SAMLSettingsResourceModelV1) toAPIModel(ctx context.Context, apiModel *SAMLSettingsAPIModel) diag.Diagnostics {
diags := diag.Diagnostics{}

apiModel.Name = r.Name.ValueString()
apiModel.Enable = r.Enable.ValueBool()
Expand All @@ -85,10 +86,19 @@ func (r *SAMLSettingsResourceModelV1) toAPIModel(_ context.Context, apiModel *SA
apiModel.VerifyAudienceRestriction = r.VerifyAudienceRestriction.ValueBool()
apiModel.UseEncryptedAssertion = r.UseEncryptedAssertion.ValueBool()

return nil
var ldapGroupSettings []string
d := r.LDAPGroupSettings.ElementsAs(ctx, &ldapGroupSettings, false)
if d.HasError() {
diags.Append(d...)
return diags
}

apiModel.LDAPGroupSettings = ldapGroupSettings

return diags
}

func (r *SAMLSettingsResourceModelV1) fromAPIModel(_ context.Context, apiModel *SAMLSettingsAPIModel) (ds diag.Diagnostics) {
func (r *SAMLSettingsResourceModelV1) fromAPIModel(ctx context.Context, apiModel *SAMLSettingsAPIModel) (ds diag.Diagnostics) {
r.Name = types.StringValue(apiModel.Name)
r.Enable = types.BoolValue(apiModel.Enable)
r.Certificate = types.StringValue(apiModel.Certificate)
Expand Down Expand Up @@ -117,29 +127,39 @@ func (r *SAMLSettingsResourceModelV1) fromAPIModel(_ context.Context, apiModel *
r.VerifyAudienceRestriction = types.BoolValue(apiModel.VerifyAudienceRestriction)
r.UseEncryptedAssertion = types.BoolValue(apiModel.UseEncryptedAssertion)

ldapGroupSettings := types.SetNull(types.StringType)
if len(apiModel.LDAPGroupSettings) > 0 {
ls, d := types.SetValueFrom(ctx, types.StringType, apiModel.LDAPGroupSettings)
if d.HasError() {
ds.Append(d...)
return
}

ldapGroupSettings = ls
}

r.LDAPGroupSettings = ldapGroupSettings

return
}

type SAMLSettingsAPIModel struct {
Name string `json:"name"`
Enable bool `json:"enable_integration"`
VerifyAudienceRestriction bool `json:"verify_audience_restriction"`
LoginURL string `json:"login_url"`
LogoutURL string `json:"logout_url"`
ServiceProviderName string `json:"service_provider_name"`
AutoUserCreation bool `json:"auto_user_creation"`
AllowUserToAccessProfile bool `json:"allow_user_to_access_profile"`
UseEncryptedAssertion bool `json:"use_encrypted_assertion"`
AutoRedirect bool `json:"auto_redirect"`
SyncGroups bool `json:"sync_groups"`
Certificate string `json:"certificate"`
GroupAttribute string `json:"group_attribute"`
EmailAttribute string `json:"email_attribute"`
NameIDAttribute string `json:"name_id_attribute"`
}

func (r *SAMLSettingsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = r.TypeName
Name string `json:"name"`
Enable bool `json:"enable_integration"`
VerifyAudienceRestriction bool `json:"verify_audience_restriction"`
LoginURL string `json:"login_url"`
LogoutURL string `json:"logout_url"`
ServiceProviderName string `json:"service_provider_name"`
AutoUserCreation bool `json:"auto_user_creation"`
AllowUserToAccessProfile bool `json:"allow_user_to_access_profile"`
UseEncryptedAssertion bool `json:"use_encrypted_assertion"`
AutoRedirect bool `json:"auto_redirect"`
SyncGroups bool `json:"sync_groups"`
Certificate string `json:"certificate"`
GroupAttribute string `json:"group_attribute"`
EmailAttribute string `json:"email_attribute"`
NameIDAttribute string `json:"name_id_attribute"`
LDAPGroupSettings []string `json:"ldap_group_settings,omitempty"`
}

var samlSettingsSchemaV0 = map[string]schema.Attribute{
Expand Down Expand Up @@ -270,6 +290,14 @@ var samlSettingsSchemaV1 = lo.Assign(
},
MarkdownDescription: "When set, authenticated users are automatically created in Artifactory. When not set, for every request from an SSO user, the user is temporarily associated with default groups (if such groups are defined), and the permissions for these groups apply. Without automatic user creation, you must manually create the user inside Artifactory to manage user permissions not attached to their default groups. Default value is `true`.",
},
"ldap_group_settings": schema.SetAttribute{
ElementType: types.StringType,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
},
Optional: true,
MarkdownDescription: "List of LDAP group setting names. Only support in Artifactory 7.98 or later. See [Enabling Synchronization of LDAP Groups for SAML SSO](https://jfrog.com/help/r/jfrog-platform-administration-documentation/enabling-synchronization-of-ldap-groups-for-saml-sso) for more details.",
},
},
)

Expand Down Expand Up @@ -314,24 +342,8 @@ func (r *SAMLSettingsResource) Configure(ctx context.Context, req resource.Confi
if req.ProviderData == nil {
return
}
r.ProviderData = req.ProviderData.(PlatformProviderMetadata)

supported, err := util.CheckVersion(r.ProviderData.ArtifactoryVersion, "7.83.1")
if err != nil {
resp.Diagnostics.AddError(
"Failed to check Artifactory version",
err.Error(),
)
return
}

if !supported {
resp.Diagnostics.AddError(
"Unsupported Artifactory version",
fmt.Sprintf("This resource is supported by Artifactory version 7.83.1 or later. Current version: %s", r.ProviderData.ArtifactoryVersion),
)
return
}
m := req.ProviderData.(PlatformProviderMetadata).ProviderMetadata
r.ProviderData = &m
}

func (r *SAMLSettingsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
Expand All @@ -353,7 +365,7 @@ func (r *SAMLSettingsResource) Create(ctx context.Context, req resource.CreateRe

response, err := r.ProviderData.Client.R().
SetBody(samlSettings).
Post(SAMLSettingsEndpoint)
Post(r.CollectionEndpoint)

if err != nil {
utilfw.UnableToCreateResourceError(resp, err.Error())
Expand Down Expand Up @@ -384,7 +396,7 @@ func (r *SAMLSettingsResource) Read(ctx context.Context, req resource.ReadReques
response, err := r.ProviderData.Client.R().
SetPathParam("name", state.Name.ValueString()).
SetResult(&samlSettings).
Get(SAMLSettingEndpoint)
Get(r.DocumentEndpoint)

if err != nil {
utilfw.UnableToRefreshResourceError(resp, err.Error())
Expand Down Expand Up @@ -433,7 +445,7 @@ func (r *SAMLSettingsResource) Update(ctx context.Context, req resource.UpdateRe
response, err := r.ProviderData.Client.R().
SetPathParam("name", plan.Name.ValueString()).
SetBody(samlSettings).
Put(SAMLSettingEndpoint)
Put(r.DocumentEndpoint)

if err != nil {
utilfw.UnableToUpdateResourceError(resp, err.Error())
Expand Down Expand Up @@ -462,7 +474,7 @@ func (r *SAMLSettingsResource) Delete(ctx context.Context, req resource.DeleteRe

response, err := r.ProviderData.Client.R().
SetPathParam("name", state.Name.ValueString()).
Delete(SAMLSettingEndpoint)
Delete(r.DocumentEndpoint)
if err != nil {
utilfw.UnableToDeleteResourceError(resp, err.Error())
return
Expand Down
10 changes: 9 additions & 1 deletion pkg/platform/resource_saml_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func TestAccSAMLSettings_full(t *testing.T) {
sync_groups = true
verify_audience_restriction = true
use_encrypted_assertion = false
ldap_group_settings = {{ .ldap_groups }}
}`

testData := map[string]string{
Expand All @@ -41,6 +42,7 @@ func TestAccSAMLSettings_full(t *testing.T) {
"group_attribute": "group",
"name_id_attribute": "name",
"auto_user_creation": "false",
"ldap_groups": "[\"test-group-1\"]",
}

config := util.ExecuteTemplate(name, temp, testData)
Expand All @@ -52,6 +54,7 @@ func TestAccSAMLSettings_full(t *testing.T) {
"group_attribute": "group2",
"name_id_attribute": "name2",
"auto_user_creation": "true",
"ldap_groups": "[\"test-group-1\", \"test-group-2\"]",
}

updatedConfig := util.ExecuteTemplate(name, temp, updatedTestData)
Expand Down Expand Up @@ -79,6 +82,8 @@ func TestAccSAMLSettings_full(t *testing.T) {
resource.TestCheckResourceAttr(fqrn, "sync_groups", "true"),
resource.TestCheckResourceAttr(fqrn, "verify_audience_restriction", "true"),
resource.TestCheckResourceAttr(fqrn, "use_encrypted_assertion", "false"),
resource.TestCheckResourceAttr(fqrn, "ldap_group_settings.#", "1"),
resource.TestCheckResourceAttr(fqrn, "ldap_group_settings.0", "test-group-1"),
),
},
{
Expand All @@ -99,6 +104,9 @@ func TestAccSAMLSettings_full(t *testing.T) {
resource.TestCheckResourceAttr(fqrn, "sync_groups", "true"),
resource.TestCheckResourceAttr(fqrn, "verify_audience_restriction", "true"),
resource.TestCheckResourceAttr(fqrn, "use_encrypted_assertion", "false"),
resource.TestCheckResourceAttr(fqrn, "ldap_group_settings.#", "2"),
resource.TestCheckResourceAttr(fqrn, "ldap_group_settings.0", "test-group-1"),
resource.TestCheckResourceAttr(fqrn, "ldap_group_settings.1", "test-group-2"),
),
},
{
Expand Down Expand Up @@ -126,7 +134,7 @@ func testAccSamlSettingsDestroy(id string) func(*terraform.State) error {
resp, err := c.R().
SetPathParam("name", rs.Primary.Attributes["name"]).
SetResult(&samlSettings).
Get(platform.SAMLSettingEndpoint)
Get("access/api/v1/saml/{name}")
if err != nil {
return err
}
Expand Down

0 comments on commit 03800ce

Please sign in to comment.