IDM Toolbox: howto create missing OUs on the fly

1 Likes
A while ago I ran into the need for dynamic OU creation on a new driver and remembered having suggested a piece of code that was supposed to do that on the forums not long before. So I grabbed that rule and tried it myself - only to find it's not working due to at least three bugs. After fixing and testing for a while, it seemed a better idea to start over again from scratch.

I wanted a rule that is fast and efficient. It should only kick in if really required and just do the bare minimum amount of processing - no brute-force algorithm that slows down processing and messes up traces too much. Here's what I finally came up with:
<rule>
<description>Create missing containers on the fly</description>
<comment xml:space="preserve">This policy creates missing containers that are needed to perform the current add or move operation. All containers are created as class "Organizational Unit". If your tree structure uses different classes, change the policy accordingly. It is assumed that all DNs are in slash format. This policy should be used as a publisher command transform policy.</comment>
<conditions>
<or>
<if-xpath op="true">self::add/@dest-dn</if-xpath>
<if-xpath op="true">self::move/parent[not(association)]/@dest-dn</if-xpath>
</or>
</conditions>
<actions>
<do-set-local-variable name="missing-containers">
<arg-node-set/>
</do-set-local-variable>
<do-set-local-variable name="parent-dn">
<arg-string>
<token-xpath expression="self::move/parent/@dest-dn"/>
<token-parse-dn dest-dn-format="slash" length="-2" src-dn-format="slash">
<token-xpath expression="self::add/@dest-dn"/>
</token-parse-dn>
</arg-string>
</do-set-local-variable>
<do-while>
<arg-conditions>
<and>
<if-local-variable mode="regex" name="parent-dn" op="equal">. </if-local-variable>
<if-xpath op="not-true">query:readObject($destQueryProcessor,'',$parent-dn,'','')</if-xpath>
</and>
</arg-conditions>
<arg-actions>
<do-set-local-variable name="missing-containers">
<arg-node-set>
<token-local-variable name="parent-dn"/>
<token-local-variable name="missing-containers"/>
</arg-node-set>
</do-set-local-variable>
<do-set-local-variable name="parent-dn">
<arg-string>
<token-parse-dn dest-dn-format="slash" length="-2" src-dn-format="slash">
<token-local-variable name="parent-dn"/>
</token-parse-dn>
</arg-string>
</do-set-local-variable>
</arg-actions>
</do-while>
<do-for-each>
<arg-node-set>
<token-local-variable name="missing-containers"/>
</arg-node-set>
<arg-actions>
<do-add-dest-object class-name="Organizational Unit" when="before">
<arg-dn>
<token-local-variable name="current-node"/>
</arg-dn>
</do-add-dest-object>
</arg-actions>
</do-for-each>
</actions>
</rule>

Let's look at the rule in detail, step by step:

  • The conditions only let the rule kick in if basic sanity checks are successful, i.e. we have a DN that tells us which container(s) we might need to create on the fly.

  • move operations that specify the target container through an association reference are ignored, since any container with an association must already exist

  • the missing-containers variable is initialized as an empty nodeset, so we won't get an additional empty node when later adding the first missing DN in the while loop (if left undefined here would later resolve to an empty string turning into an empty node, instead of an empty nodeset).

  • the parent-dn variable is initialized by concatenation of the two possible dest-dn values as they could've passed the rule's condition group. Since the operation must be either add or move, one of them will always be an empty string. This could be implemented through an "if operation=add then parent-dn=... else parent-dn=..." construct as well, but this is a one-liner and saves a couple of mouse-clicks while editing without becoming hard to follow.

  • all DN values are assumed to be in slash format. If anything else is used, conversion with token-parse-dn needs to be added

  • Testing for the existence of the parent containers is done from leaf to root and stopped as soon as an existing container is found. This ensures we do only the minimum necessary amount of edir lookups compared to checking all DN parts and/or checking in root-to-leaf order. Whenever a container does not exist it's DN is added to the missing-containers nodeset variable.

  • Finally, OUs are created from the missing-containers nodeset by adding them before the current operation to the input doc. We could as well add them with the direct="true" flag, but I find the trace easier to read this way and it also allows for subsequent rules to modify those add operations before actually writing to the vault.


Now this is a very basic approach, but a few possible enhancements come to mind:

  • This rule could be placed in a library object and linked to all drivers that need to create containers on the fly.

  • A GCV could be defined and added to the rule's conditions to en/disabled dynamic container creation.

  • This rule creates "Organizational Unit" objects only. The class to create could as well be set in a GCV or - if different classes at different tree levels are required - in a mapping table that maps container distance from root against the object class to be created. The parent-dn would have to be normalized in that case (either always with or without the tree name at the beginning) and the depth level determined before creating the object.

  • Of course, the default container class used when no mapping table entry is found, could again be set as a GCV.


All of the above added, the rule would look like this:
<rule>
<description>Create missing containers on the fly (global)</description>
<comment xml:space="preserve">This policy creates missing containers that are needed to perform the current add or move operation. The CGV "dynamic-container-creation" must be set to "true" to enable this rule and the GCV "dynamic-container-default-class" must be set. If containers shall be created as different classes depending on the tree depth, they must be defined in the "dynamic-container-classes" mapping table. It is assumed that all DNs are in slash format. This rule should be placed in a library object and linked into driver configs as a publisher command transform policy.</comment>
<conditions>
<or>
<if-global-variable mode="nocase" name="dynamic-container-creation" op="equal">true</if-global-variable>
</or>
<or>
<if-xpath op="true">self::add/@dest-dn</if-xpath>
<if-xpath op="true">self::move/parent[not(association)]/@dest-dn</if-xpath>
</or>
</conditions>
<actions>
<do-set-local-variable name="missing-containers">
<arg-node-set/>
</do-set-local-variable>
<do-set-local-variable name="parent-dn" scope="policy">
<arg-string>
<token-replace-first regex="^\\~dirxml.auto.treename~\\" replace-with="">
<token-xpath expression="self::move/parent/@dest-dn"/>
<token-parse-dn dest-dn-format="slash" length="-2" src-dn-format="slash">
<token-xpath expression="self::add/@dest-dn"/>
</token-parse-dn>
</token-replace-first>
</arg-string>
</do-set-local-variable>
<do-while>
<arg-conditions>
<and>
<if-local-variable mode="regex" name="parent-dn" op="equal">. </if-local-variable>
<if-xpath op="not-true">query:readObject($destQueryProcessor,'',$parent-dn,'','')</if-xpath>
</and>
</arg-conditions>
<arg-actions>
<do-set-local-variable name="missing-containers">
<arg-node-set>
<token-local-variable name="parent-dn"/>
<token-local-variable name="missing-containers"/>
</arg-node-set>
</do-set-local-variable>
<do-set-local-variable name="parent-dn">
<arg-string>
<token-parse-dn dest-dn-format="slash" length="-2" src-dn-format="slash">
<token-local-variable name="parent-dn"/>
</token-parse-dn>
</arg-string>
</do-set-local-variable>
</arg-actions>
</do-while>
<do-for-each>
<arg-node-set>
<token-local-variable name="missing-containers"/>
</arg-node-set>
<arg-actions>
<do-set-local-variable name="parent-class" scope="policy">
<arg-string>
<token-map default-value="~dynamic-container-default-class~" dest="Class" src="Level" table="..\dynamic-container-classes">
<token-xpath expression='string-length(translate($current-node,"\","*"))'/>
</token-map>
</arg-string>
</do-set-local-variable>
<do-add-dest-object class-name="$parent-class$" when="before">
<arg-dn>
<token-local-variable name="current-node"/>
</arg-dn>
</do-add-dest-object>
</arg-actions>
</do-for-each>
</actions>
</rule>

Personally, I find the first version much nicer to look at, but I am admittedly a fan of minimalistic rules. The second version has the potential to become a standard driver feature, though, at least in my environment.

And finally, here's a trace (of the basic version) to see it in action:
[...]:DemoDriver PT:
<nds dtdversion="2.0" ndsversion="8.x">
<source>
<product build="20090520_001502" instance="DemoDriver" version="3.5.4">Identity Manager Driver for Lotus Notes</product>
<contact>Novell, Inc.</contact>
</source>
<input>
<move class-name="Group" dest-dn="data\idm\groups\de\muenchen\groups_storage\Testgroup" dest-entry-id="..." event-id="...">
<association>E2D4087C0055768DB32C128D1BA6494C</association>
<parent dest-dn="\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage"/>
</move>
</input>
</nds>
[...]:DemoDriver PT:Applying policy: 'pub-ctp: create missing containers'.
[...]:DemoDriver PT: Applying to move #1.
[...]:DemoDriver PT: Evaluating selection criteria for rule 'Create missing containers on the fly'.
[...]:DemoDriver PT: (if-xpath true "self::add/@dest-dn") = FALSE.
[...]:DemoDriver PT: (if-xpath true "self::move/parent[not(association)]/@dest-dn") = TRUE.
[...]:DemoDriver PT: Rule selected.
[...]:DemoDriver PT: Applying rule 'Create missing containers on the fly'.
[...]:DemoDriver PT: Action: do-set-local-variable("missing-containers",arg-node-set()).
[...]:DemoDriver PT: arg-node-set()
[...]:DemoDriver PT: Arg Value: {}.
[...]:DemoDriver PT: Action: do-set-local-variable("parent-dn",token-xpath("self::move/parent/@dest-dn") token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-xpath("self::add/@dest-dn"))).
[...]:DemoDriver PT: arg-string(token-xpath("self::move/parent/@dest-dn") token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-xpath("self::add/@dest-dn")))
[...]:DemoDriver PT: token-xpath("self::move/parent/@dest-dn")
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage".
[...]:DemoDriver PT: token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-xpath("self::add/@dest-dn"))
[...]:DemoDriver PT: token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-xpath("self::add/@dest-dn"))
[...]:DemoDriver PT: token-xpath("self::add/@dest-dn")
[...]:DemoDriver PT: Token Value: "".
[...]:DemoDriver PT: Arg Value: "".
[...]:DemoDriver PT: Token Value: "".
[...]:DemoDriver PT: Arg Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage".
[...]:DemoDriver PT: Action: do-while().
[...]:DemoDriver PT: Evaluating conditions.
[...]:DemoDriver PT: (if-local-variable 'parent-dn' match ". ") = TRUE.
[...]:DemoDriver PT: Query from policy
[...]:DemoDriver PT:
<nds dtdversion="3.5" ndsversion="8.x">
<source>
<product version="3.6.10.4747">DirXML</product>
<contact>Novell, Inc.</contact>
</source>
<input>
<query dest-dn="\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage" scope="entry">
<read-attr/>
</query>
</input>
</nds>
[...]:DemoDriver PT: Pumping XDS to eDirectory.
[...]:DemoDriver PT: Performing operation query for \IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage.
[...]:DemoDriver PT: Query from policy result
[...]:DemoDriver PT:
<nds dtdversion="3.5" ndsversion="8.x">
<source>
<product version="3.6.10.4747">DirXML</product>
<contact>Novell, Inc.</contact>
</source>
<output>
<status event-id="0" level="success"></status>
</output>
</nds>
[...]:DemoDriver PT: (if-xpath not-true "query:readObject($destQueryProcessor,'',$parent-dn,'','')") = TRUE.
[...]:DemoDriver PT: Performing while actions.
[...]:DemoDriver PT: Action: do-set-local-variable("missing-containers",arg-node-set(token-local-variable("parent-dn") token-local-variable("missing-containers"))).
[...]:DemoDriver PT: arg-node-set(token-local-variable("parent-dn") token-local-variable("missing-containers"))
[...]:DemoDriver PT: token-local-variable("parent-dn")
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage".
[...]:DemoDriver PT: token-local-variable("missing-containers")
[...]:DemoDriver PT: Token Value: {}.
[...]:DemoDriver PT: Arg Value: {"\IDM-TEST-TREE\data\idm\groups\de\leipzig\g..."}.
[...]:DemoDriver PT: Action: do-set-local-variable("parent-dn",token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn"))).
[...]:DemoDriver PT: arg-string(token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn")))
[...]:DemoDriver PT: token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn"))
[...]:DemoDriver PT: token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn"))
[...]:DemoDriver PT: token-local-variable("parent-dn")
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage".
[...]:DemoDriver PT: Arg Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage".
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: Arg Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: Evaluating conditions.
[...]:DemoDriver PT: (if-local-variable 'parent-dn' match ". ") = TRUE.
[...]:DemoDriver PT: Query from policy
[...]:DemoDriver PT:
<nds dtdversion="3.5" ndsversion="8.x">
<source>
<product version="3.6.10.4747">DirXML</product>
<contact>Novell, Inc.</contact>
</source>
<input>
<query dest-dn="\IDM-TEST-TREE\data\idm\groups\de\leipzig" scope="entry">
<read-attr/>
</query>
</input>
</nds>
[...]:DemoDriver PT: Pumping XDS to eDirectory.
[...]:DemoDriver PT: Performing operation query for \IDM-TEST-TREE\data\idm\groups\de\leipzig.
[...]:DemoDriver PT: Query from policy result
[...]:DemoDriver PT:
<nds dtdversion="3.5" ndsversion="8.x">
<source>
<product version="3.6.10.4747">DirXML</product>
<contact>Novell, Inc.</contact>
</source>
<output>
<status event-id="0" level="success"></status>
</output>
</nds>
[...]:DemoDriver PT: (if-xpath not-true "query:readObject($destQueryProcessor,'',$parent-dn,'','')") = TRUE.
[...]:DemoDriver PT: Performing while actions.
[...]:DemoDriver PT: Action: do-set-local-variable("missing-containers",arg-node-set(token-local-variable("parent-dn") token-local-variable("missing-containers"))).
[...]:DemoDriver PT: arg-node-set(token-local-variable("parent-dn") token-local-variable("missing-containers"))
[...]:DemoDriver PT: token-local-variable("parent-dn")
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: token-local-variable("missing-containers")
[...]:DemoDriver PT: Token Value: {"\IDM-TEST-TREE\data\idm\groups\de\leipzig\g..."}.
[...]:DemoDriver PT: Arg Value: {"\IDM-TEST-TREE\data\idm\groups\de\leipzig","\IDM-TEST-TREE\data\idm\groups\de\leipzig\g..."}.
[...]:DemoDriver PT: Action: do-set-local-variable("parent-dn",token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn"))).
[...]:DemoDriver PT: arg-string(token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn")))
[...]:DemoDriver PT: token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn"))
[...]:DemoDriver PT: token-parse-dn(dest-dn-format="slash",length="-2",src-dn-format="slash",token-local-variable("parent-dn"))
[...]:DemoDriver PT: token-local-variable("parent-dn")
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: Arg Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de".
[...]:DemoDriver PT: Arg Value: "\IDM-TEST-TREE\data\idm\groups\de".
[...]:DemoDriver PT: Evaluating conditions.
[...]:DemoDriver PT: (if-local-variable 'parent-dn' match ". ") = TRUE.
[...]:DemoDriver PT: Query from policy
[...]:DemoDriver PT:
<nds dtdversion="3.5" ndsversion="8.x">
<source>
<product version="3.6.10.4747">DirXML</product>
<contact>Novell, Inc.</contact>
</source>
<input>
<query dest-dn="\IDM-TEST-TREE\data\idm\groups\de" scope="entry">
<read-attr/>
</query>
</input>
</nds>
[...]:DemoDriver PT: Pumping XDS to eDirectory.
[...]:DemoDriver PT: Performing operation query for \IDM-TEST-TREE\data\idm\groups\de.
[...]:DemoDriver PT: Query from policy result
[...]:DemoDriver PT:
<nds dtdversion="3.5" ndsversion="8.x">
<source>
<product version="3.6.10.4747">DirXML</product>
<contact>Novell, Inc.</contact>
</source>
<output>
<instance class-name="Organizational Unit" event-id="0" qualified-src-dn="O=data\OU=idm\OU=groups\OU=de" src-dn="\IDM-TEST-TREE\data\idm\groups\de" src-entry-id="..."/>
<status event-id="0" level="success"></status>
</output>
</nds>
[...]:DemoDriver PT: (if-xpath not-true "query:readObject($destQueryProcessor,'',$parent-dn,'','')") = FALSE.
[...]:DemoDriver PT: Action: do-for-each(arg-node-set(token-local-variable("missing-containers"))).
[...]:DemoDriver PT: arg-node-set(token-local-variable("missing-containers"))
[...]:DemoDriver PT: token-local-variable("missing-containers")
[...]:DemoDriver PT: Token Value: {"\IDM-TEST-TREE\data\idm\groups\de\leipzig","\IDM-TEST-TREE\data\idm\groups\de\leipzig\g..."}.
[...]:DemoDriver PT: Arg Value: {"\IDM-TEST-TREE\data\idm\groups\de\leipzig","\IDM-TEST-TREE\data\idm\groups\de\leipzig\g..."}.
[...]:DemoDriver PT: Performing actions for local-variable(current-node) = "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: Action: do-add-dest-object(class-name="Organizational Unit",when="before",arg-dn(token-local-variable("current-node"))).
[...]:DemoDriver PT: arg-dn(token-local-variable("current-node"))
[...]:DemoDriver PT: token-local-variable("current-node")
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: Arg Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig".
[...]:DemoDriver PT: Performing actions for local-variable(current-node) = "\IDM-TEST-TREE\data\idm\groups\de\leipzig\g...".
[...]:DemoDriver PT: Action: do-add-dest-object(class-name="Organizational Unit",when="before",arg-dn(token-local-variable("current-node"))).
[...]:DemoDriver PT: arg-dn(token-local-variable("current-node"))
[...]:DemoDriver PT: token-local-variable("current-node")
[...]:DemoDriver PT: Token Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage".
[...]:DemoDriver PT: Arg Value: "\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage".
[...]:DemoDriver PT:Policy returned:
[...]:DemoDriver PT:
<nds dtdversion="2.0" ndsversion="8.x">
<source>
<product build="20090520_001502" instance="DemoDriver" version="3.5.4">Identity Manager Driver for Lotus Notes</product>
<contact>Novell, Inc.</contact>
</source>
<input>
<add class-name="Organizational Unit" dest-dn="\IDM-TEST-TREE\data\idm\groups\de\leipzig" event-id="..."/>
<add class-name="Organizational Unit" dest-dn="\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage" event-id="..."/>
<move class-name="Group" dest-dn="data\idm\groups\de\muenchen\groups_storage\Testgroup" dest-entry-id="..." event-id="...">
<association>E2D4087C0055768DB32C128D1BA6494C</association>
<parent dest-dn="\IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage"/>
</move>
</input>
</nds>
[...]:DemoDriver PT:Filtering out notification-only attributes.
[...]:DemoDriver PT:Pumping XDS to eDirectory.
[...]:DemoDriver PT:Performing operation add for \IDM-TEST-TREE\data\idm\groups\de\leipzig.
[...]:DemoDriver PT:Adding entry \IDM-TEST-TREE\data\idm\groups\de\leipzig.
[...]:DemoDriver PT:Creating RDN leipzig in context \IDM-TEST-TREE\data\idm\groups\de.
[...]:DemoDriver PT:Performing operation add for \IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage.
[...]:DemoDriver PT:Adding entry \IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage.
[...]:DemoDriver PT:Creating RDN groups_storage in context \IDM-TEST-TREE\data\idm\groups\de\leipzig.
[...]:DemoDriver PT:Performing operation move for data\idm\groups\de\muenchen\groups_storage\Testgroup.
[...]:DemoDriver PT:Moving entry \IDM-TEST-TREE\data\idm\groups\de\muenchen\groups_storage\Testgroup to \IDM-TEST-TREE\data\idm\groups\de\leipzig\groups_storage.
[...]:DemoDriver PT:
DirXML Log Event -------------------
Driver: \IDM-TEST-TREE\system\idm\drvset01\DemoDriver
Channel: Publisher
Object: (data\idm\groups\de\muenchen\groups_storage\Testgroup)
Status: Success
[...]:DemoDriver PT:
DirXML Log Event -------------------
Driver: \IDM-TEST-TREE\system\idm\drvset01\DemoDriver
Channel: Publisher
Object: (data\idm\groups\de\muenchen\groups_storage\Testgroup)
Status: Success
[...]:DemoDriver PT:
DirXML Log Event -------------------
Driver: \IDM-TEST-TREE\system\idm\drvset01\DemoDriver
Channel: Publisher
Object: (data\idm\groups\de\muenchen\groups_storage\Testgroup)
Status: Success

Labels:

How To-Best Practice
Comment List
  • Hi Geoffrey,

    the lookup table has just the two columns mentioned in the rule: "Level" and "Class", where the level is the distance from root (e.g. \TREE\org\company would be at level 3) and the class is the object class to be created:

    <mapping-table>
    <col-def name="Level" type="numeric"/>
    <col-def name="Class" type="nocase"/>
    <row>
    <col>1</col>
    <col>Country</col>
    </row>
    <row>
    <col>2</col>
    <col>Organization</col>
    </row>
    <row>
    <col>3</col>
    <col>Organizational Unit</col>
    </row>
    </mapping-table>


    I am not sure how special characters will be escaped in slash notation when xpath is operating on them as a variable value and if the translate function would see an escaped forward slash ("\/") as two characters or just as the "/" it's meant to be. If someone's silly enough to place animals inside containers he has to try it out, I guess...
  • I am missing something (probably what your mapping table (..\dynamic-container-classes) looks likes) that would explain this segment of your second version of the code:

    <token-map default-value="~dynamic-container-default-class~" dest="Class" src="Level" table="..\dynamic-container-classes">
    <token-xpath expression='string-length(translate($current-node,"\","*"))'/>
    </token-map>


    You are translating a backslash delimited DN, replacing all \ with *'s and then counting its length? Ah, since the only place backslashes should be in the path is to represent a divider, it would be replaced with an asterisk (Asterix is a Gaul adventurer), that you then count the number of.

    What happens if there is a period, comma, or forward slash in a OU DN? Like:
    OU="Orgs. For Sheep/Goats" that would be represented as Orgs\. For Sheep\/Goats, would it not?

    That would affect your count I think.

    Excellent approach, I feel like it is worth dissecting this one for tips and tricks to learn from it that you did not focus on. :)
Related
Recommended