l-with / terraform-provider-ldap

7 stars 4 forks source link

Attribute Or Value Exists when apply the same code #75

Closed fsdrw08 closed 4 months ago

fsdrw08 commented 4 months ago

Seem that the provider cannot fetch some specific attributes before make changes.

I use opendj, you can run it via docker, here is a docker compose example, ref: OpenDJ Preparation

  opendj:
    image: openidentityplatform/opendj:latest
    hostname: opendj.example.com
    ports:
      - "1389:1389"
      - "1636:1636"
      - "4444:4444"
    volumes:
      - ./opendj/bootstrap/data/:/opt/opendj/bootstrap/data #initial data
      - ./persistence/opendj:/opt/opendj/data #opendj data
    environment:
      - BASE_DN=dc=openidentityplatform,dc=org #should be yours base DN

the ldif of initial data in my case

dn: ou=People,dc=openidentityplatform,dc=org
objectClass: organizationalunit
objectClass: top
ou: People

after docker-compose up, run the terraform to apply ldap entry
provider information:

# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/l-with/ldap" {
  version     = "0.5.6"
  constraints = ">= 0.5.6"
  hashes = [
    "h1:cqTRL6dknCj0b6yLFX+4ZNFFMnuqbpJ0PcAaE6smQ0k=",
  ]
}

provider config:

provider "ldap" {
 ...

  bind_user     = "cn=Directory Manager"
  bind_password = "password"
}

terraform code:

resource "ldap_entry" "user_admin" {
  dn = "uid=admin,ou=People,dc=openidentityplatform,dc=org "
  data_json = jsonencode({
    objectClass       = ["top", "inetOrgPerson", "organizationalPerson", "person"]
    userPassword      = ["{SSHA}9tYIWTF0A7+ipgncHEJJhRL9Vb/pydxL4A=="]
    cn                = ["admin"]
    sn                = ["admin"]
    ds-privilege-name = ["password-reset"]
  })
}

the first time apply, it return:

# ldap_entry.user_admin will be created
  + resource "ldap_entry" "user_admin" {
      + data_json = jsonencode(
            {
              + cn                = [
                  + "admin",
                ]
              + ds-privilege-name = [
                  + "password-reset",
                ]
              + objectClass       = [
                  + "top",
                  + "inetOrgPerson",
                  + "organizationalPerson",
                  + "person",
                ]
              + sn                = [
                  + "admin",
                ]
              + userPassword      = [
                  + "{SSHA}9tYIWTF0A7+ipgncHEJJhRL9Vb/pydxL4A==",
                ]
            }
        )
      + dn        = "uid=admin,ou=People,dc=openidentityplatform,dc=org "
      + id        = (known after apply)
    }

then re-apply again, it will shows

# ldap_entry.user_admin will be updated in-place
  ~ resource "ldap_entry" "user_admin" {
      ~ data_json = jsonencode(
          ~ {
              + ds-privilege-name = [
                  + "password-reset",
                ]
                # (4 unchanged attributes hidden)
            }
        )
        id        = "uid=admin,ou=People,dc=openidentityplatform,dc=org "
        # (1 unchanged attribute hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
ldap_entry.user_admin: Modifying... [id=uid=admin,ou=People,dc=openidentityplatform,dc=org]
╷
│ Error: LDAP Result Code 20 "Attribute Or Value Exists": Entry uid=admin,ou=People,dc=openidentityplatform,dc=org  cannot be modified because it would have resulted in one or more duplicate values for attribute ds-privilege-name: password-reset
│
│   with ldap_entry.user_admin,
│   on main.tf line 2, in resource "ldap_entry" "user_admin":
│    2: resource "ldap_entry" "user_admin" {
│
╵
fsdrw08 commented 4 months ago

here are some debug info:


ldap_entry.svc_readonly: Refreshing state... [id=uid=readonly,ou=Services,dc=openidentityplatform,dc=org]
2024-05-12T10:52:25.749+0800 [WARN]  Provider "registry.terraform.io/l-with/ldap" produced an unexpected new value for ldap_entry.user_admin during refresh.
      - .data_json: was cty.StringVal("{\"cn\":[\"admin\"],\"ds-privilege-name\":[\"password-reset\"],\"objectClass\":[\"top\",\"inetOrgPerson\",\"organizationalPerson\",\"person\"],\"sn\":[\"admin\"],\"userPassword\":[\"{SSHA}9tYIWTF0A7+ipgncHEJJhRL9Vb/pydxL4A==\"]}"), 
      - but now cty.StringVal("{\"cn\":[\"admin\"],\"objectClass\":[\"top\",\"inetOrgPerson\",\"organizationalPerson\",\"person\"],\"sn\":[\"admin\"],\"userPassword\":[\"{SSHA}9tYIWTF0A7+ipgncHEJJhRL9Vb/pydxL4A==\"]}")
2024-05-12T10:52:25.759+0800 [DEBUG] provider.stdio: received EOF, stopping recv loop: err="rpc error: code = Unavailable desc = error reading from server: EOF"
2024-05-12T10:52:25.766+0800 [DEBUG] provider: plugin process exited: path=.terraform/providers/registry.terraform.io/l-with/ldap/0.5.6/windows_amd64/terraform-provider-ldap_v0.5.6.exe pid=18244
2024-05-12T10:52:25.767+0800 [DEBUG] provider: plugin exited
2024-05-12T10:52:25.768+0800 [DEBUG] building apply graph to check for errors```
fsdrw08 commented 4 months ago

The problem is the attribute ds-privilege-name is hided if we not specified it during the ldap search action, so we should specified all attributes key list in the tf code during the ldap search action.

I think we need to update the attributes parameter here https://github.com/l-with/terraform-provider-ldap/blob/main/client/entry.go#L102 may be need to load the attributes key from status in before the ReadEntryByDN request

fsdrw08 commented 4 months ago

some ideas:

var localLdapData map[string]interface{} err := json.Unmarshal([]byte(localDataJson.(string)), &localLdapData) if err != nil { panic(err) }

var attributeKeys []string for key := range localLdapData { attributeKeys = append(attributeKeys, key) }

ldapEntry, err := cl.ReadEntryByDN(id, "("+dummyFilter+")", attributeKeys)


- Add attributes param in [func (c *Client) ReadEntryByDN](https://github.com/l-with/terraform-provider-ldap/blob/main/client/entry.go#L90-L104)
```go
func (c *Client) ReadEntryByDN(
    dn string,
    filter string,
        attributes []string,
) (ldapEntry *LdapEntry, err error) {
    req := ldap.NewSearchRequest(
        dn,
        ldap.ScopeBaseObject,
        ldap.NeverDerefAliases,
        0,
        0,
        false,
        filter,
        attributes,
        []ldap.Control{},
    )
l-with commented 4 months ago

I tried to reproduce this case. I struggle where to place the ldif config before starting docker-compose. Please describe.

By now I get:

Entry uid=admin,ou=People,dc=openidentityplatform,dc=org cannot be added because its parent entry ou=People,dc=openidentityplatform,dc=org

fsdrw08 commented 4 months ago

I tried to reproduce this case. I struggle where to place the ldif config before starting docker-compose. Please describe.

By now I get:

Entry uid=admin,ou=People,dc=openidentityplatform,dc=org cannot be added because its parent entry ou=People,dc=openidentityplatform,dc=org

according to the docker-compose

  opendj:
    image: openidentityplatform/opendj:latest
    hostname: opendj.example.com
    ports:
      - "1389:1389"
      - "1636:1636"
      - "4444:4444"
    volumes:
      - ./opendj/bootstrap/data/:/opt/opendj/bootstrap/data #initial data
      - ./persistence/opendj:/opt/opendj/data #opendj data
    environment:
      - BASE_DN=dc=openidentityplatform,dc=org #should be yours base DN

You should put the init ldif data in the current docker-compose dir's sub dir of ./opendj/bootstrap/data/

l-with commented 4 months ago

You should put the init ldif data in the current docker-compose dir's sub dir of ./opendj/bootstrap/data/

Is there a naming convention?

fsdrw08 commented 4 months ago

You should put the init ldif data in the current docker-compose dir's sub dir of ./opendj/bootstrap/data/

Is there a naming convention?

it should be no naming convention

l-with commented 4 months ago

some ideas:

localDataJson = d.Get(attributeNameDataJson)

var localLdapData map[string]interface{}
err := json.Unmarshal([]byte(localDataJson.(string)), &localLdapData)
if err != nil {
  panic(err)
}

var attributeKeys []string
for key := range localLdapData {
  attributeKeys = append(attributeKeys, key)
}

ldapEntry, err := cl.ReadEntryByDN(id, "("+dummyFilter+")", attributeKeys)
func (c *Client) ReadEntryByDN(
  dn string,
  filter string,
        attributes []string,
) (ldapEntry *LdapEntry, err error) {
  req := ldap.NewSearchRequest(
      dn,
      ldap.ScopeBaseObject,
      ldap.NeverDerefAliases,
      0,
      0,
      false,
      filter,
      attributes,
      []ldap.Control{},
  )

Looks like an accurate analysis. I am wondering if the case sensitive will play a role. Passing the attributes to an ldap search request may not request the attributes if have a different spelling in the ldap server.

l-with commented 4 months ago

should be fixed in version 0.8.0