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
-
search for existing tenants in ou=tenants
-
in each tenant search for ou=permissions under ou=pdp. Here the place holder @forEach@ is used, which is the tenant DN
-
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.
-
then finally an entry is created after having resolved all place holders
if ou=permissions does not yet exists
- add ou=permissions
- 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.
- Syntax:
- 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.
- Syntax:
- 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.
- Syntax:
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.