diff --git a/docs/data-sources/ou.md b/docs/data-sources/ou.md new file mode 100644 index 0000000..e4c5ebc --- /dev/null +++ b/docs/data-sources/ou.md @@ -0,0 +1,25 @@ +# ldap_ou + +`ldap_ou` is a data source for managing an LDAP OU. + +## Example Usage + +```hcl +data "ldap_ou" "ou" { + ou = "OU=MyCompany,DC=domain,DC=tld" + name = "MyOU" + scope = 2 +} +``` + +## Argument Reference + +* `name` - (Required) LDAP OU name. +* `ou` - (Required) OU where LDAP OU will be search. +* `scope` - (Optional) LDAP search scope (0: BaseObject, 1: SingleLevel, 2: WholeSubtree) Defaults to `0`. + +## Attribute Reference + +* `id` - The DN of the LDAP OU. +* `description` - Description attribute for the LDAP OU +* `managed_by` - ManagedBy attribute. \ No newline at end of file diff --git a/docs/resources/ou.md b/docs/resources/ou.md new file mode 100644 index 0000000..36ad573 --- /dev/null +++ b/docs/resources/ou.md @@ -0,0 +1,32 @@ +# ldap_ou + +`ldap_ou` is a resource for managing an LDAP OU. + +## Example Usage + +```hcl +resource "ldap_ou" "ou" { + name = "MyOU" + ou = "OU=MyCompany,DC=domain,DC=tld" + description = "My OU description" +} +``` + +## Argument Reference + +* `ou` - (Required) OU where LDAP OU will be created. +* `name` - (Required) LDAP OU name. +* `description` - (Optional) Description attribute for the LDAP OU. Defaults to empty. +* `managed_by` - (Optional) ManagedBy attribute. Defaults to ``. + +## Attribute Reference + +* `id` - The DN of the LDAP OU. + +## Import + +LDAP OU can be imported using the full LDAP DN (id), e.g. + +``` +$ terraform import ldap_ou.example OU=Myou,OU=MyCompany,DC=domain,DC=tld +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 9f09400..1d0c2dc 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Ouest-France/terraform-provider-ldap go 1.18 require ( - github.com/Ouest-France/goldap v0.6.2 + github.com/Ouest-France/goldap v0.6.4 github.com/go-ldap/ldap/v3 v3.4.4 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 ) diff --git a/go.sum b/go.sum index 063311e..f85ecc8 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Ouest-France/goldap v0.6.2 h1:8uTl/RSbTf/yjHRyhrSRHARSXgaqXqGZmTnB5EODAa8= -github.com/Ouest-France/goldap v0.6.2/go.mod h1:1oPqn1er8HgJglFFCRpY+puY6mnSJQYVxb77wMuCxAA= +github.com/Ouest-France/goldap v0.6.4 h1:YZDVku6NpYm5UTtue0iWsucMNHNzx7BLSoIm81CtoFE= +github.com/Ouest-France/goldap v0.6.4/go.mod h1:1oPqn1er8HgJglFFCRpY+puY6mnSJQYVxb77wMuCxAA= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= diff --git a/ldap/data_source_resource_ldap_ou.go b/ldap/data_source_resource_ldap_ou.go new file mode 100644 index 0000000..cef18d3 --- /dev/null +++ b/ldap/data_source_resource_ldap_ou.go @@ -0,0 +1,74 @@ +package ldap + +import ( + "context" + "fmt" + + "github.com/Ouest-France/goldap" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceLDAPOU() *schema.Resource { + return &schema.Resource{ + Description: "`ldap_ou` is a data source for getting an LDAP OU.", + ReadContext: dataSourceLDAPOURead, + + Schema: map[string]*schema.Schema{ + "id": { + Description: "The DN of the LDAP OU.", + Type: schema.TypeString, + Computed: true, + }, + "ou": { + Description: "OU where LDAP OU will be search.", + Type: schema.TypeString, + Required: true, + }, + "name": { + Description: "LDAP OU name.", + Type: schema.TypeString, + Required: true, + }, + "scope": { + Description: "LDAP search scope", + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "description": { + Description: "Description attribute for the LDAP OU", + Type: schema.TypeString, + Computed: true, + }, + "managed_by": { + Description: "ManagedBy attribute", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceLDAPOURead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + + // Get scope + scope := d.Get("scope").(int) + if scope < 0 || scope > 2 { + return diag.FromErr(fmt.Errorf("scope must be between 0 and 2, got %d", scope)) + } + + // If scope is 1 or 2, we search the OU DN given the OU name and the OU + client := m.(*goldap.Client) + + // Search OU + dn, err := client.SearchOUByName(d.Get("name").(string), d.Get("ou").(string), scope) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(dn) + + // Add context key to signal the Read is called from a datasource + return resourceLDAPOURead(context.WithValue(ctx, CallerTypeKey, DatasourceCaller), d, m) +} diff --git a/ldap/provider.go b/ldap/provider.go index 41b74ea..c10fe8d 100644 --- a/ldap/provider.go +++ b/ldap/provider.go @@ -53,10 +53,12 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ "ldap_group": resourceLDAPGroup(), + "ldap_ou": resourceLDAPOU(), }, DataSourcesMap: map[string]*schema.Resource{ "ldap_group": dataSourceLDAPGroup(), "ldap_user": dataSourceLDAPUser(), + "ldap_ou": dataSourceLDAPOU(), }, ConfigureFunc: providerConfigure, } diff --git a/ldap/resource_ldap_ou.go b/ldap/resource_ldap_ou.go new file mode 100644 index 0000000..4131e34 --- /dev/null +++ b/ldap/resource_ldap_ou.go @@ -0,0 +1,156 @@ +package ldap + +import ( + "context" + "fmt" + "strings" + + "github.com/Ouest-France/goldap" + "github.com/go-ldap/ldap/v3" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceLDAPOU() *schema.Resource { + return &schema.Resource{ + Description: "`ldap_ou` is a resource for managing an LDAP OU.", + CreateContext: resourceLDAPOUCreate, + ReadContext: resourceLDAPOURead, + UpdateContext: resourceLDAPOUUpdate, + DeleteContext: resourceLDAPOUDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "id": { + Description: "The DN of the LDAP OU.", + Type: schema.TypeString, + Computed: true, + }, + "ou": { + Description: "OU where LDAP OU will be created.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Description: "LDAP OU name.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "description": { + Description: "Description attribute for the LDAP OU.", + Type: schema.TypeString, + Optional: true, + }, + "managed_by": { + Description: "ManagedBy attribute", + Type: schema.TypeString, + Optional: true, + Default: "", + }, + }, + } +} + +func resourceLDAPOUCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*goldap.Client) + + dn := fmt.Sprintf("OU=%s,%s", d.Get("name").(string), d.Get("ou").(string)) + + err := client.CreateOrganizationalUnit(dn, d.Get("description").(string), d.Get("managed_by").(string)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(dn) + + return resourceLDAPOURead(ctx, d, m) +} + +func resourceLDAPOURead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*goldap.Client) + + dn := d.Id() + + attributes, err := client.ReadOrganizationalUnit(dn) + if err != nil { + if err.(*ldap.Error).ResultCode == 32 { + // Object doesn't exist + + // If Read is called from a datasource, return an error + if ctx.Value(CallerTypeKey) == DatasourceCaller { + return diag.Errorf("LDAP OU not found: %s", dn) + } + + // If not a call from datasource, remove the resource from the state + // and cleanly return + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + nameAttr, ok := attributes["ou"] + if !ok || len(nameAttr) != 1 { + return diag.Errorf("LDAP attribute \"name\" doesn't exist or is empty for OU: %s", dn) + } + + if err := d.Set("name", nameAttr[0]); err != nil { + return diag.FromErr(err) + } + + // Remove the `OU=,` from the DN to get the OU + ou := strings.ReplaceAll(dn, fmt.Sprintf("OU=%s,", attributes["name"][0]), "") + if err := d.Set("ou", ou); err != nil { + return diag.FromErr(err) + } + + desc := "" + if val, ok := attributes["description"]; ok { + desc = val[0] + } + if err := d.Set("description", desc); err != nil { + return diag.FromErr(err) + } + + managedBy := "" + if val, ok := attributes["managedBy"]; ok { + managedBy = val[0] + } + if err := d.Set("managed_by", managedBy); err != nil { + return diag.FromErr(err) + } + + return diag.FromErr(err) +} + +func resourceLDAPOUUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*goldap.Client) + dn := fmt.Sprintf("OU=%s,%s", d.Get("name").(string), d.Get("ou").(string)) + + if d.HasChange("description") { + if err := client.UpdateOrganizationalUnitDescription(dn, d.Get("description").(string)); err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("managed_by") { + if err := client.UpdateOrganizationalUnitManagedBy(dn, d.Get("managed_by").(string)); err != nil { + return diag.FromErr(err) + } + } + + return resourceLDAPOURead(ctx, d, m) +} + +func resourceLDAPOUDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*goldap.Client) + dn := fmt.Sprintf("OU=%s,%s", d.Get("name").(string), d.Get("ou").(string)) + + err := client.DeleteOrganizationalUnit(dn) + + return diag.FromErr(err) +}