Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

PATCH: complete ldap search filters



This adds a helper function for completing LDAP search filters
conforming to RFC4515. Also included is completion for the OpenLDAP
client utilities (ldapsearch etc). There are alternate implementations
of some of these (such as in OpenDS) but I've only ever used those from
OpenLDAP. Finally, there is a helper for LDAP attributes. These can in
theory be anything - they are just database keys. But in practice there
probably isn't so much variation. I've taken a lazy approach of dumping
all keys from both a FreeIPA and OpenLDAP installation - both for the
attributes and classes.

It still needs some adapting to work properly with backslash quoting. I've
made it force double-quoting by default which is more useful for the
filters anyway. For some reason making the filter argument in _arguments
optional causes it to add an extra layer of quoting to & and |; for now,
I've not marked them as optional. I've also used an obscure workaround
of completing an empty string with a prefix to avoid quotes being
removed:
  _foo() {
    compset -P \*
    compadd -P '' \)
    compadd -P \( one two
  }
  foo '(&(a=b)<tab> → removes the quote
  The workaround is to use compadd -P '\)' ''

Oliver

diff --git a/Completion/BSD/Command/_ldap b/Completion/BSD/Command/_ldap
index 8fa17e2f8..181e6b0d0 100644
--- a/Completion/BSD/Command/_ldap
+++ b/Completion/BSD/Command/_ldap
@@ -80,8 +80,8 @@ else
         '-x[use simple authentication]' \
         '-Z[use StartTLS]' \
         '-z+[specify maximum number of results or 0 for no limit]:size limit [0]:' \
-        '::filter:' \
-        '*:attribute:'
+        '1: :_ldap_filters' \
+        '*: :_ldap_attributes'
       ;;
   esac
 fi
diff --git a/Completion/Unix/Command/_openldap b/Completion/Unix/Command/_openldap
new file mode 100644
index 000000000..233d0950e
--- /dev/null
+++ b/Completion/Unix/Command/_openldap
@@ -0,0 +1,222 @@
+#compdef ldapadd ldapcompare ldapdelete ldapexop ldapmodify ldapmodrdn ldappasswd ldapsearch ldapurl ldapwhoami
+
+local curcontext="$curcontext" nm="$compstate[nmatches]"
+local -a args auth state line expl
+
+args=( '*-e[general extensions]:extension:->general-extensions' )
+
+case $service in
+  ldapadd|ldapcompare|ldapdelete|ldapexop|ldapmodify|ldapmodrdn|ldappasswd|ldapsearch|ldapwhoami)
+    if (( $words[(I)-[^Z]#Z[^Z]#] )); then
+      args+=( '*-Z[require success for start TLS request]' )
+    elif (( ! $words[(I)-[^Z]#Z] )); then
+      args+=( '-Z[start TLS request]' )
+    fi
+    args+=(
+      '!(-)-VV' '-V[display version information]'
+      '*-d+[set LDAP debugging level]:level:((1\:trace 2\:packets 4\:args 8\:conns 10\:ber 2048\:parse -1\:all))'
+      "-n[show what would be done but don't actually do it]"
+      '-v[verbose output]'
+      "-N[don't use reverse DNS to canonicalize SASL host name]"
+      '*-o+[specify any ldap.conf options]: : _values option
+        "ldif_wrap[specify width]\:width"
+        "nettimeout[specify timeout]\:timeout (seconds)"'
+    )
+    auth=(
+      '-D[specify bind DN]:binddn'
+      '-H[specify LDAP URIs]:uri'
+      '-P[specify protocol version]:version [3]:(2 3)'
+      + simple
+      '(sasl)-x[use simple authentication]'
+      '(sasl -W -y)-w+[specify bind password]:bind password'
+      '(sasl -w -y)-W[prompt for bind password]'
+      '(sasl -w -W)-y+[read password from file]:file:_files'
+      + sasl
+      '(simple)-O+[specify SASL security properties]: : _values -s , property
+          none noplain noactive nodict noanonymous forwardsec passcred
+          minssf\:factor maxssf\:factor maxbufsize\:factor'
+      '(simple)-X+[specify SASL authorization identity]:authzid:->authzids'
+      '(simple)-Y+[specify SASL mechanism]:mechanism:compadd -M "m:{a-zA-Z}={A-Za-z}" EXTERNAL GSSAPI' # iana has a full list but cyrus support seems limited
+      '(simple)-R+[specify SASL realm]:realm'
+      '(simple)-U+[specify SASL authentication identity]:authcid'
+      '(simple)-I[use SASL Interactive mode]'
+      '(simple)-Q[use SASL Quiet mode]'
+    )
+  ;|
+  ldapadd|ldapcompare|ldapdelete|ldapmodify|ldapmodrdn|ldapsearch)
+    if (( $words[(I)-[^M]#M[^M]#] )); then
+      args+=( '*-M[enable Manage DSA IT control critical]' )
+    elif (( ! $words[(I)-[^M]#M] )); then
+      args+=( '-M[enable Manage DSA IT control]' )
+    fi
+  ;|
+  ldapadd|ldapdelete|ldapmodify|ldapmodrdn|ldapsearch)
+    # ldapexop documents but doesn't implement this
+    args+=( '(1 2 *)-f+[read operations from file]:file:_files' )
+  ;|
+  ldapadd|ldapdelete|ldapmodify|ldapmodrdn|ldapsearch)
+    args+=( "-c[continuous operation mode (don't stop on errors)]" )
+  ;|
+  ldapdelete|ldapsearch)
+    args+=( '-z+[specify size limit]:size limit (entries)' )
+  ;|
+  ldapadd|ldapmodify)
+    args+=(
+      '-S+[write records that are skipped due to an error to file]:file:_files'
+      '*-E+[modify extensions]:extension:->modify-extensions'
+    )
+  ;|
+  ldapurl|ldapsearch)
+    args+=(
+      '(decompose)-s+[specify search scope]:search scope [sub]:(base one sub children)'
+    )
+  ;|
+  ldapdelete|ldapmodrdn|ldapurl|ldapwhoami) args+=( '!*-E+:extension' ) ;|
+
+  ldapadd) args+=( '!-a' ) ;;
+  ldapmodify) args+=( '-a[add new entries]' ) ;;
+  ldapcompare)
+    args+=(
+      '-z[quiet mode - no output aside return status]'
+      '*-E+[compare extensions]:extension:->compare-extensions'
+    )
+  ;;
+  ldapdelete)
+    args+=(
+      '-r[do a recursive delete]'
+      '*: :_guard "^-*" "distinguished name"'
+    )
+  ;;
+  ldapexop) args+=( '*:: :->extended-operations' ) ;;
+  ldapmodrdn)
+    args+=(
+      '-r[remove old RDN values from the entry]'
+      '-s[specify new superior entry to move target to]:entry'
+      '1:distinguished name'
+      '2:relative distinguished name'
+    )
+  ;;
+  ldappasswd)
+    args+=(
+      '(-a -t)-A[prompt for old password]'
+      '(-A -t)-a+[specify old password]:password'
+      '(-A -a)-t+[read old password from file]:file:_files'
+      '(-s -T)-S[prompt for new password]'
+      '(-S -T)-s+[specify new password]:password'
+      '(-S -s)-T+[read new password from file]:file:_files'
+    )
+  ;;
+  ldapsearch)
+    if (( $words[(I)-[^L]#L[^L]#L[^L]#] )); then
+      args+=( '*-L[LDIF format without comments and version]' )
+    elif (( $words[(I)-[^L]#L[^L]#] )); then
+      args+=( '*-L[LDIF format without comments]' )
+    elif ! (( $words[(I)-[^L]#L[^L]#L[^L]#L] )); then
+      args+=( '-L[LDIFv1 format]' )
+    else
+      args+=( '!*-L' )
+    fi
+    if (( $words[(I)-[^t]#t[^t]#] )); then
+      args+=( '*-t[write all retrieved values to files in temporary directory]' )
+    elif (( ! $words[(I)-[^t]#t] )); then
+      args+=( '-t[write binary values to files in temporary directory]' )
+    fi
+
+    args+=(
+      '-a+[specify how aliases dereferencing is done]:deref [never]:(never always search find)'
+      '-A[retrieve attributes only (no values)]'
+      '-b+[specify base dn for search]:basedn'
+      '*-E+[search extensions]:extension:->search-extensions'
+      '-F+[specify URL prefix for temporary files]:prefix [file:///tmp//]'
+      '-l+[specify time limit for search]:time limit (seconds)'
+      '-S+[sort results by specified attribute]:attribute:_ldap_attributes'
+      '-T[write files to specified directory]:path [/tmp]:_directories'
+      '-u[include User Friendly entry names in the output]'
+      '1: :_ldap_filters'
+      '2: : _alternative
+        "attributes:attribute:_ldap_attributes"
+        "attributes:attribute:((1.1\:no\ attributes \*\:all\ user\ attributes \+\:all\ operational\ attributes))"'
+      '*:attribute:_ldap_attributes -F line'
+    )
+  ;;
+  ldapurl)
+    args+=(
+      - compose
+      '-a+[set a list of attribute selectors]:attribute selectors (comma separated)'
+      '-b+[set the searchbase]:search base'
+      '-f+[set the URL filter]:filter:_ldap_filters'
+      '-h+[set the host]:host:_hosts'
+      '-p+[set the tcp port]:port:(389 636)'
+      '-S+[set the URL scheme]:scheme:(ldap ldaps)'
+      - decompose
+      '(-s)-H+[specify URI to be exploded]:uri'
+    )
+  ;;
+esac
+
+_arguments -C -S -s $args $auth
+
+case $state in
+  extended-operations)
+    case $CURRENT:$words[1] in
+      1:*)
+        if compset -P '*::'; then
+          _message -e data 'base64 data'
+        elif compset -P '*:'; then
+          _message -e data data
+        else
+          _alternative \
+            'oids::_guard "(<->(|.))#" oid' \
+            'operations:operation:(whoami cancel refresh)'
+        fi
+      ;;
+      2:cancel) _message -e ids 'cancel id' ;;
+      2:refresh) _message -e names 'distinguished name' ;;
+      3:refresh) _message -e times 'ttl' ;;
+      *) _message 'no more arguments' ;;
+    esac
+  ;;
+  *-extensions)
+    if ! compset -P \!; then
+      _description criticality expl critical
+      compadd -S "" "$expl[@]" \!
+    fi
+  ;|
+  modify-extensions) _values extension 'txn:txn:(abort commit)' ;;
+  compare-extensions) _values extension dontUseCopy ;;
+  search-extensions)
+    _values extension \
+      'mv[matched values filter]:filter:_ldap_filters' \
+      'pr[paged results/prompt]:size[/prompt|noprompt]' \
+      'sss[server side sorting]: :_sequence -s / _ldap_attributes' \
+      'subentries: :(true false)' \
+      'sync:sync[/cookie][/slimit]:((ro\:refreshOnly rp\:refreshAndPersist))' \
+      'vlv[virtual list view]:before/after(/offset/count|\:value' \
+      'deref:derefAttr:_sequence _ldap_attributes' \
+      dontUseCopy domainScope
+  ;;
+  general-extensions)
+    _values extension \
+      'assert:filter:_ldap_filters' \
+      'authzid:authzid:->authzids' \
+      {post,pre}'read: :_sequence _ldap_attributes' \
+      'sessiontracking:username:_users' \
+      'chaining:behavior:(chainingPreferred chainingRequired referralsPreferred referralsRequired)' \
+      bauthzid manageDSAit noop ppolicy relax abandon cancel ignore
+  ;&
+  authzids)
+    if [[ $state != authzids ]]; then
+      : # fall-through from above without the authzids state
+    elif compset -P 'u:'; then
+      _description users expl authzid
+      _users "$expl[@]"
+    elif compset -P 'dn:'; then
+      _message -e ids 'distinguished name'
+    else
+      _description prefixes expl prefix
+      compadd -S: "$expl[@]" u dn
+    fi
+  ;;
+esac
+
+[[ nm -ne "$compstate[nmatches]" ]]
diff --git a/Completion/Unix/Type/_ldap_attributes b/Completion/Unix/Type/_ldap_attributes
new file mode 100644
index 000000000..0711cfbf1
--- /dev/null
+++ b/Completion/Unix/Type/_ldap_attributes
@@ -0,0 +1,27 @@
+#autoload
+
+local -a expl attrs
+
+# These come from dumping attributes from basic installations of both openldap
+# and FreeIPA and combining results. It is possible to have custom additions so
+# a definitive list is not possible Hence the use of -x with compadd.
+#
+attrs=(
+  associatedDomain authenticationMethod automountInformation automountKey
+  automountMapName bindTimeLimit cACertificate;binary cn dc defaultSearchBase
+  defaultServerList description displayName dn followReferrals gecos gidNumber
+  givenName homeDirectory info initials ipaCertIssuerSerial ipaCertSubject
+  ipaConfigString ipaKeyExtUsage ipaKeyTrust ipaNTSecurityIdentifier
+  ipaPublicKey ipaUniqueID ipHostNumber loginShell mail member memberUid
+  mepManagedBy nisDomain nisNetgroupTriple o objectClass objectClassMap ou
+  pwdAllowUserChange pwdAttribute pwdCheckQuality pwdExpireWarning
+  pwdFailureCountInterval pwdGraceAuthNLimit pwdInHistory pwdLockout
+  pwdLockoutDuration pwdMaxAge pwdMaxFailure pwdMinAge pwdMinLength
+  pwdMustChange pwdSafeModify searchTimeLimit serviceSearchDescriptor sn
+  telephoneNumber uid uidNumber userCertificate;binary userPKCS12
+  userSMIMECertificate
+)
+
+_description ldap-attributes expl "ldap attribute"
+compadd "${@:/-X/-x}" "${expl[@]:/-X/-x}" \
+    -M 'm:{a-zA-Z}={A-Za-z} r:[^A-Z]||[A-Z]=* r:|=*' -a attrs
diff --git a/Completion/Unix/Type/_ldap_filters b/Completion/Unix/Type/_ldap_filters
new file mode 100644
index 000000000..919f9e266
--- /dev/null
+++ b/Completion/Unix/Type/_ldap_filters
@@ -0,0 +1,91 @@
+#autoload
+
+# LDAP search filters conforming to RFC4515
+
+local -a expl excl optype disp end pre
+local -i nest=0
+local open='(' close=')'
+
+[[ -prefix - ]] && return 1
+
+local -a matchingrules=( # From RFC4517
+  bitStringMatch booleanMatch caseExactIA5Match
+  caseExactMatch caseExactOrderingMatch caseExactSubstringsMatch
+  caseIgnoreIA5Match caseIgnoreIA5SubstringsMatch caseIgnoreListMatch
+  caseIgnoreListSubstringsMatch caseIgnoreMatch caseIgnoreOrderingMatch
+  caseIgnoreSubstringsMatch directoryStringFirstComponentMatch
+  distinguishedNameMatch generalizedTimeMatch generalizedTimeOrderingMatch
+  integerFirstComponentMatch integerMatch integerOrderingMatch keywordMatch
+  numericStringMatch numericStringOrderingMatch numericStringSubstringsMatch
+  objectIdentifierFirstComponentMatch objectIdentifierMatch octetStringMatch
+  octetStringOrderingMatch telephoneNumberMatch telephoneNumberSubstringsMatch
+  uniqueMemberMatch wordMatch
+)
+local -a classes=( # Sampled from real servers, arbitrary other values allowed
+  automount automountMap cosTemplate dcObject device dnaSharedConfig domain
+  domainRelatedObject DUAConfigProfile extensibleObject groupOfNames
+  groupOfPrincipals ieee802device inetOrgPerson inetuser ipaassociation ipaca
+  ipacaacl ipaCertificate ipaCertMapConfigObject ipacertprofile ipaConfigObject
+  ipaDomainIDRange ipaDomainLevelConfig ipaGuiConfig ipahbacrule ipahbacservice
+  ipahbacservicegroup ipahost ipahostgroup ipaIDrange ipaKeyPolicy
+  ipakrbprincipal ipaNameResolutionData ipaNTDomainAttrs ipaNTGroupAttrs
+  ipaNTUserAttrs ipaobject ipaPublicKeyObject ipaReplTopoManagedServer
+  ipaservice ipaSshGroupOfPubKeys ipasshhost ipasshuser ipasudorule
+  ipaSupportedDomainLevelConfig ipaTrustedADDomainRange ipaUserAuthTypeClass
+  ipausergroup ipHost krbContainer krbprincipal krbprincipalaux
+  krbrealmcontainer krbTicketPolicyAux mepManagedEntry mepOriginEntry
+  nestedGroup nisDomainObject nisNetgroup nsContainer nsDS5Replica nshost
+  organization organizationalPerson organizationalRole organizationalUnit
+  person pilotObject pkiCA pkiuser posixAccount posixGroup pwdPolicy
+  shadowAccount simpleSecurityObject top
+)
+
+compquote open close
+open=${(q)open} close=${(q)close}
+# default to double rather than backslash quoting
+[[ -z $compstate[quote] || -z $PREFIX ]] && pre='"('
+
+zstyle -s ":completion:${curcontext}:operators" list-separator sep || sep=--
+print -v disp -f "%s $sep %s" \| or \& and \! not
+end=( ") $sep end" )
+excl=( \\\| \& ) # compadd -F uses globs: only | needs quoting
+
+local -a query=(
+  \( /$'*\0[ \t\n]#'/ \) # strip off any preceding arguments
+  \(
+    \( "/${open}!/" -'optype[++nest]=0;pre=""'
+    \| "/${open}\|/" -'optype[++nest]=1;pre=""'
+    \| "/${open}&/" -'optype[++nest]=2;pre=""'
+    \| '/[]/' ':operators:operator:compadd -F "( ${(q)excl[optype[nest]]} )" -d disp -P ${pre:-${(Q)open}} -S \( \| \& \!' \)
+  \|
+    \( '/[^)]##/' '%\)%' # pass over whole var=value, needed due to lack of backtracking after the following
+    \| "/${open}(#i)homeDirectory=/" '/[]/' ':directories:directory:_directories -P / -W / -r ") \t\n\-"'
+    \| "/${open}(#i)loginShell=/" '/[]/' ':shells:shell:compadd -S ${(Q)close} ${(f)^"$(</etc/shells)"}(N)'
+    \| "/${open}(#i)mail=/" '/[]/' ':email-addresses:mail:_email_addresses -S ${(Q)close}'
+    \| "/${open}(#i)objectClass=/" '/[]/' ':object-classes:class:compadd -S ${(Q)close} -M "m:{a-zA-Z}={A-Za-z} r:[^A-Z]||[A-Z]=* r:|=*" -a classes'
+    \| "/${open}(#i)(automountKey|(member|)uid)=/" '/[]/' ':users:username:_users -S ${(Q)close}'
+    \| "/${open}(#i)cn=/" '/[]/' ':cn:cn: _alternative "users:user:_users -S ${close}" "groups:group:_groups -S ${close}" "hosts:host:_hosts -S ${close}"'
+    \|
+      '/[^:=<>~]##/' '%[=:<>~]%' -'pre=""'
+      ':object-types:object type:_ldap_attributes -P ${pre:-${(Q)open}}  -S = -r ":=~<> \t\n\-"'
+      \(
+        '/:/'
+        '/[^:]##:=/' ':matching-rules:matching rule:compadd -S ":=" -a matchingrules'
+      \|
+        '/([~<>]|)=/' ':operators:operator:compadd -S "" "<=" \>= \~='
+      \)
+      '/[^)]##/' '%\)%' ': _message -e object-values "object value (* for presence check)"'
+    \)
+    "/$close/" -'(( nest ))' ':brackets:bracket:compadd ${=query[nest]:+-S ""} \)'
+    \(
+      # This use of -P/-d and an empty match works around a limitation/bug where
+      # mixed use of -P removes any quoting
+      "/$close/" ':operators:operator:compadd ${=query[nest-1]:+-S ""} -d end -P ${(Q)close} ""'
+      \( // -'(( --nest ))' \| '//' -'((!nest))' '/[]/' ': compadd ""' \)
+    \) \#
+    // -'(( nest && optype[nest] ))'
+  \) \#
+)
+
+_regex_arguments _ldap_search_filters "$query[@]"
+_ldap_search_filters




Messages sorted by: Reverse Date, Date, Thread, Author