From mboxrd@z Thu Jan 1 00:00:00 1970 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on inbox.vuxu.org X-Spam-Level: X-Spam-Status: No, score=-1.6 required=5.0 tests=DKIM_SIGNED,DKIM_VALID, DKIM_VALID_AU,LONGWORDS,MAILING_LIST_MULTI,RCVD_IN_DNSWL_MED autolearn=ham autolearn_force=no version=3.4.4 Received: (qmail 6794 invoked from network); 17 Feb 2023 23:47:30 -0000 Received: from zero.zsh.org (2a02:898:31:0:48:4558:7a:7368) by inbox.vuxu.org with ESMTPUTF8; 17 Feb 2023 23:47:30 -0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=zsh.org; s=rsa-20210803; h=List-Archive:List-Owner:List-Post:List-Unsubscribe: List-Subscribe:List-Help:List-Id:Sender:Message-ID:Date: Content-Transfer-Encoding:Content-ID:Content-Type:MIME-Version:Subject:To: From:Reply-To:Cc:Content-Description:Resent-Date:Resent-From:Resent-Sender: Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References; bh=WWYRL/93v+blGfKwOeYYxIoRhGMF3GlGTnL7pWvG0wY=; b=N+xL9tBclX36BrQJFwNc8gjdUd 2PfiP+5zYdH/lzphTv8HkK7KxZZq+GKGzQIYn3BF50yIXMFK9gYEsZ+/bBYFO9D0N1gvdMdwrEOb/ xeclXnrPLDM1NeFutfODCJ7xIYQtkbUXD8ci0yRaoXUbRRAD7PJRLxqEy19gyEgXpcSXVTT2F5td5 4tKd9plGQqciaMqWic0/sCeQ/fs64BbGWs+MFbdaSco/VBdMecstKqgvEi88XJT+D8XYaFTb8FlZs +wwOLO3RyDIjI6Q/FHimgJopMfRfALrS/6Rv/6DGA0hWVifXKxjPD6Zs8e+xUCZ9LrdR6xTG6+cs1 +ILYkD1w==; Received: by zero.zsh.org with local id 1pTARt-000Cnw-OR; Fri, 17 Feb 2023 23:47:29 +0000 Received: by zero.zsh.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) id 1pTARh-000CU4-1N; Fri, 17 Feb 2023 23:47:17 +0000 Received: from [192.168.178.21] (helo=hydra) by mail.kiddle.eu with esmtp(Exim 4.95) (envelope-from ) id 1pTARg-000NLq-9L for zsh-workers@zsh.org; Sat, 18 Feb 2023 00:47:16 +0100 From: Oliver Kiddle To: Zsh workers Subject: PATCH: complete ldap search filters MIME-Version: 1.0 Content-Type: text/plain; charset="UTF-8" Content-ID: <89764.1676677636.1@hydra> Content-Transfer-Encoding: 8bit Date: Sat, 18 Feb 2023 00:47:16 +0100 Message-ID: <89765-1676677636.286144@jiXR.VaXn.4CXW> X-Seq: 51455 Archived-At: X-Loop: zsh-workers@zsh.org Errors-To: zsh-workers-owner@zsh.org Precedence: list Precedence: bulk Sender: zsh-workers-request@zsh.org X-no-archive: yes List-Id: List-Help: , List-Subscribe: , List-Unsubscribe: , List-Post: List-Owner: List-Archive: 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) → 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)^"$(~]##/' '%[=:<>~]%' -'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