Skip to content
Snippets Groups Projects

didmos2-openldap

This is the base OpenLDAP docker container for didmos2 and provides a basic OpenLDAP with common schemas.

Migrations

A migration mechanism is available, that performs static changes in LDAP by loading LDIF-files as well as a dynamic migration of already existing customer data in LDAP.

General Features

  • Load of LDIF-files with or without changetype in the LDIF-data (default is add)
  • Configuration of the dynamic migration in json format

LDIF-Migrations

You can use normal ldap syntax which the exeption that between every block and also at the end of the file 2 newlines are required.

JSON-Migration-Features

A Json block has one or more of the following items, which will be executed in the given order

delete

"delete": [
  { "dn": "<DN>" }
]

rename

"rename": [
  { "dn": "<OLD_DN>",
    "newdn": "<NEW_DN>" }
]

add

"add": [
  { "dn": "ou=permissions,@parent.baseDN@",
    "attributes": [
      { "name": "<ATTRIBUTE_1>",
        "value": ["<VALUE_1>", "<VALUE_2>"]
      },
      { "name": "<ATTRIBUTE_2>",
        "value": ["<VALUE_3>", "<VALUE_4>"]
      }
    ]
  }
]

modify

Note: in case of delete the value is optional. If no value is provided all values will be deleted for the given attribute.

"modify": [
  { "dn": "rbacName=admin-permission,@parent.baseDN@",
    "attributes": [
      { "name": "<ATTRIBUTE_1>",
        "value": ["<VALUE_1>", "<VALUE_2>"],
        "op": "add | delete | modify | replace"
      }
    ]
  }
],

search

baseDN and searchFilter are required. scope is optional. if No scope is provided it defaults to sub. forEach and else are also optional. Inside of forEach and else you have access to the placeholder @parent.baseDN@. In forEach you additionally have access to the placeholder @forEach@.

"search": {
  "baseDN": "ou=tenants,ou=data,ou=root-tenant,dc=didmos,dc=de",
  "searchFilter": "(&(objectClass=didmosTenant)(objectClass=organizationalUnit))",
  "scope": "one | base | sub",
  "forEach": {

  },
  "else": {

  }
}

continue on errors

This works for add, delete, modify and rename.

You can set per operation to ignoreErrors and continue with the execution. Imagine you want to add 2 new values to and attribute where 1 value already might be existing. If you always want to get the missing values added you will have to split it in 2 seperate operations like so:

{
	"version": "1",
  "modify": [
    {
      "dn": "rbacName=defaultuser-modify-permission,ou=permissions,ou=pdp,ou=root-tenant,dc=didmos,dc=de",
      "attributes": [
        { "name": "rbacConstraint",
          "value": ["EXISTINGVALUE"],
          "op": "add"
        }
      ],
      "ignoreErrors": true
    },
    {
      "dn": "rbacName=defaultuser-modify-permission,ou=permissions,ou=pdp,ou=root-tenant,dc=didmos,dc=de",
      "attributes": [
        { "name": "rbacConstraint",
          "value": ["NEWVALUE"],
          "op": "add"
        }
      ]
    }
  ]
}

Placeholders and extension methods

  • Use dynamic built-in variables in place holders
  • Support of method calls with arguments and use the result to resolve place holders
  • Method arguments can be built again on their part by a method call
  • Already existing methods:
    • ext_generate_uuid() -> str: generate an UUID
    • ext_explode_dn(dn: str, count: int) -> str: cuts the DN by count RDN's
    • ext_search(search_base_dn: str, search_filter: str, ret_attr: str = "-") -> list: searches in search_base_dn for search_filter and returns a the attribute ret_attr or the DN (default) as a list
    • ext_intersection(self, *lists) -> list: returns the intersection of lists of strings

Future JSON-Migration-features

  • Additional methods can be easily implemented to be used in configuration files

Complex Json configuration examples

{
  "version": "1",
  "search": {
    "baseDN": "ou=tenants,ou=data,ou=root-tenant,dc=didmos,dc=de",
    "searchFilter": "(&(objectClass=didmosTenant)(objectClass=organizationalUnit))",

    "forEach": {
      "search": {
        "baseDN": "ou=pdp,@forEach@",
        "searchFilter": "(&(ou=permissions)(objectClass=organizationalUnit))",
        "scope": "one",
  
        "forEach": {
          "functions": [
            { "name": "ext_explode_dn", "args": ["@forEach@", 2], "result": "tenant.DN" },
            { "name": "ext_generate_uuid", "args": "", "result": "UUID1" },
            { "name": "ext_intersection", "result": "rbacResourceDN",
              "args": [
              { "name": "ext_search",
                "args": ["ou=people,ou=data,@tenant.DN@", "(&(objectClass=didmosPerson)(sn=Admin))"] },
              { "name": "ext_search",
                "args": ["ou=roles,@parent.baseDN@", "(rbacName=admin)", "rbacPerformer"] }
              ]
            }
          ],
          "add": [
            {
              "dn": "rbacName=@UUID1@,@forEach@",
              "attributes": [
                { "name": "objectClass", "value": ["rbacPermission"] },
                { "name": "rbacName", "value": "@UUID1@" },
                { "name": "rbacRoleDN", "value": "rbacName=defaultuser,ou=roles,@parent.baseDN@" },
                { "name": "rbacConstraint", "value": "attribute:sn" },
                { "name": "rbacOperation", "value": [ "read" ] },
                { "name": "rbacResourceDN", "value": "@rbacResourceDN@"}
              ]
            }
          ]
        },
        "else": {
          "functions": [
            {
              "name": "ext_generate_uuid",
              "args": "",
              "result": "UUID1"
            },
            {
              "name": "ext_search",
              "args": ["ou=roles,@parent.baseDN@", "(&(objectClass=rbacRole)(rbacDisplayName=defaultuser))"],
              "result": "DEFUALTUSERROLEDN"
            }
          ],
          "add": [
            { "dn": "ou=permissions,@parent.baseDN@",
              "attributes": [
              { "name": "objecClass",
                "value": ["organizationalUnit"],
              },
              { "name": "ou",
                "value": ["permissions"],
              }
            },
            {
              "dn": "rbacName=@UUID1@,ou=permissions,@parent.baseDN@",
              "attributes": [
                { "name": "rbacName",
                  "value": ["@UUID1@"],
                },
                { "name": "objecClass",
                  "value": ["rbacPermission"],
                },
                { "name": "rbacPermissionString",
                  "value": ["self"],
                }
                { "name": "rbacOperations",
                  "value": ["read", "write", "modify-del", "modify-replace", "modify-add"],
                }
                { "name": "rbacRoleDN",
                  "value": ["@DEFUALTUSERROLEDN@"],
                }
              ]
            }
          ]
        }
      }
    }
  }
}

In the example above

  1. search for existing tenants in ou=tenants

  2. in each tenant search for ou=permissions under ou=pdp. Here the place holder @forEach@ is used, which is the tenant DN

  3. in each ou=permissions (obiously only one!) first a couple of methods are called to set variables used later: tenant.DN, UUID1, rbacResourceDN. Note that the latter one uses method calls as arguments. Note also that a just created result can be used in the evaluation of the next one.

  4. then finally an entry is created after having resolved all place holders

if ou=permissions does not yet exists

  1. add ou=permissions
  2. add one permission object for the defaultuser role

Modify, delete and rename are supported as well and shown in the next example:

{
  "version": "1",
  "search": {
    "baseDN": "ou=tenants,ou=data,ou=root-tenant,dc=didmos,dc=de",
    "searchFilter": "(&(objectClass=didmosTenant)(objectClass=organizationalUnit))",

    "forEach": {
      "search": {
        "baseDN": "ou=pdp,@forEach@",
        "searchFilter": "(&(ou=permissions)(objectClass=organizationalUnit))",
        "scope": "one",
  
        "forEach": {
          "add": [
            {
              "dn": "rbacName=defaultuser-permission,@forEach@",
              "attributes": [
                { "name": "objectClass", "value": ["rbacPermission"] },
                { "name": "rbacName", "value": "defaultuser-permission" },
                { "name": "rbacRoleDN", "value": "rbacName=defaultuser,ou=roles,@parent.baseDN@" },
                { "name": "rbacOperation", "value": [ "delete", "modify-add", "modify-del", "modify-replace",
                                    "read", "readHistory", "write"
                                  ] },
                { "name": "rbacPermissionString", "value": "self" }
              ]
            },
            {
              "dn": "rbacName=groupMember-permission,@parent.baseDN@",
              "attributes": [
                { "name": "objectClass", "value": ["rbacPermission"] },
                { "name": "rbacName", "value": "groupMember-permission" },
                { "name": "rbacRoleDN",
                  "value": {
                  "search": {
                    "baseDN": "",
                    "searchFilter": "(objectClass=person)",
                    "attributes": ["entryDN"]
                  }
                  }
                },
                { "name": "rbacOperation", "value": ["read"] },
                { "name": "rbacConstraint", "value": ["attribute:cn", "member:self"] },
                { "name": "rbacPermissionFilter", "value": "(&(objectClass=didmosGroup)(member=self))" }
              ]
            }
          ],
          "modify": [
            {
              "dn": "rbacName=admin-permission,@parent.baseDN@",
              "attributes": [
                { "name": "rbacOperation",
                  "value": ["delete", "modify-add", "modify-del", "modify-replace"],
                  "op": "add"
                },
                { "name": "rbacOperation",
                  "value": ["create"],
                  "op": "delete"
                },
                { "name": "description",
                  "op": "delete"
                },
                { "name": "rbacPermissionFilter",
                  "value": ["(objectClass=*)"],
                  "op": "replace"
                }
              ]
            }
          ],
          "delete": [
            { "dn": "rbacName=dummy,@forEach@" }
          ],
          "rename": [
            { "dn": "rbacName=prod,@forEach@",
              "newdn": "rbacName=test,@forEach@" }
          ]
        }
      }
    }
  }
}

Usage

The migration scrypt's name is migration.py and resides in the top path as the current read-me file.

usage: migration.py [-h] (-l LDIF_FILE | -j CONFIG_FILE) [-H URI] [-D BIND]
                    [-w PASSWORD OR FILE] [-W] [-d] [-v] [-c]

optional arguments:
  -h, --help            show this help message and exit
  -l LDIF_FILE, --ldif LDIF_FILE
                        The ldif to import
  -j CONFIG_FILE, --json CONFIG_FILE
                        The json configuration file
  -H URI, --uri URI     The connection URI for the server (LDAP or HTTP)
  -D BIND, --bind BIND  The bind DN that is allowed to read the LDAP
                        configuration
  -w PASSWORD, --password PASSWORD
                        The password to authenticate the script for reading
                        the configuration
  -W, --ask-password    Ask for the password to authenticate the script for
                        reading the configuration
  -d, --dryrun          If set to true, planed changes are only shown but
                        changes are made to the LDAP
  -v, --verbose         If set to true, changes are shown
  -c, --continue        Continue processing even if errors occur

To Orchestrate many migrations from different files a shell script exists.

This Script calls the migration.py per ldif/json file and is able to replace variables within the files from environment variables or a file containing key-value arguments to replace the variables in the ldif/json files. If a Variable Name is not set the variable in the ldif file is set to an empty string. The Password is the ldap Password of the system.

There are three options available for the variable replacement.

  • Start the migration.sh file with Variable Names to be replaced.
    • Syntax: didmos2-openldap/migrations.sh PASSWORT '$VARIABLE1 $VARIABLE2'
    • All given variables are replaced.
  • Start the migration.sh file without a variable set.
    • Syntax: didmos2-openldap/migrations.sh PASSWORT
    • All set Environment variables are replaced. This could lead to unwanted behaviour if a variable is named to obvious names used in the linux environment. So be cautious.
  • Start the migration.sh file with a path to a file in which the variables are set
    • Syntax: didmos2-openldap/migrations.sh PASSWORT /PATH/to/file
    • All Variables form the file are replaced in the ldif files.
usage: migration.sh PASSWORD [FILE|VARIABLES] 

required arguments: 
  PASSWORD              The ldap password of the system. 

optional arguments:
  VARIABLES             A list of Variable Names which are replaced during the migration. 
  FILE                  Path to a file containing key-value pairs of variables.