Hi!

(resent to tech@)
the following ldapd patch allows filter rules to match on attributes.

This can be used to allow users to change their password (and a few
other things) but not their entire dn.

For example, in ldapd.conf:

        allow read access to any by self
        allow write access to any attribute shadowPassword by self
        allow write access to any attribute sshPublicKey by self
        allow write access to any attribute gecos by self
        allow write access to any attribute loginShell by self

Alternatively, this would also work:

        allow write access to any by self
        deny write access to any attribute mail by self

This only supports "write" (modify, add, delete) and not "read"
(search) filter rules.  The search mode will be more complicated and I
will look at this later.

Thoughts?  OK?

Reyk

Index: usr.sbin/ldapd/auth.c
===================================================================
RCS file: /cvs/src/usr.sbin/ldapd/auth.c,v
retrieving revision 1.12
diff -u -p -u -p -r1.12 auth.c
--- usr.sbin/ldapd/auth.c       20 Jan 2017 11:55:08 -0000      1.12
+++ usr.sbin/ldapd/auth.c       11 May 2018 14:09:01 -0000
@@ -33,7 +33,7 @@
 
 static int
 aci_matches(struct aci *aci, struct conn *conn, struct namespace *ns,
-    char *dn, int rights, enum scope scope)
+    char *dn, int rights, char *attr, enum scope scope)
 {
        struct btval     key;
 
@@ -98,6 +98,13 @@ aci_matches(struct aci *aci, struct conn
                        return 0;
        }
 
+       if (aci->attribute != NULL) {
+               if (attr == NULL)
+                       return 0;
+               if (strcasecmp(aci->attribute, attr) != 0)
+                       return 0;
+       }
+
        return 1;
 }
 
@@ -105,7 +112,7 @@ aci_matches(struct aci *aci, struct conn
  */
 int
 authorized(struct conn *conn, struct namespace *ns, int rights, char *dn,
-    int scope)
+    char *attr, int scope)
 {
        struct aci      *aci;
        int              type = ACI_ALLOW;
@@ -124,33 +131,41 @@ authorized(struct conn *conn, struct nam
        if ((rights & (ACI_WRITE | ACI_CREATE)) != 0)
                type = ACI_DENY;
 
-       log_debug("requesting %02X access to %s by %s, in namespace %s",
+       log_debug("requesting %02X access to %s%s%s by %s, in namespace %s",
            rights,
            dn ? dn : "any",
+           attr ? " attribute " : "",
+           attr ? attr : "",
            conn->binddn ? conn->binddn : "any",
            ns ? ns->suffix : "global");
 
        SIMPLEQ_FOREACH(aci, &conf->acl, entry) {
-               if (aci_matches(aci, conn, ns, dn, rights, scope)) {
+               if (aci_matches(aci, conn, ns, dn, rights,
+                   attr, scope)) {
                        type = aci->type;
-                       log_debug("%s by: %s %02X access to %s by %s",
+                       log_debug("%s by: %s %02X access to %s%s%s by %s",
                            type == ACI_ALLOW ? "allowed" : "denied",
                            aci->type == ACI_ALLOW ? "allow" : "deny",
                            aci->rights,
                            aci->target ? aci->target : "any",
+                           aci->attribute ? " attribute " : "",
+                           aci->attribute ? aci->attribute : "",
                            aci->subject ? aci->subject : "any");
                }
        }
 
        if (ns != NULL) {
                SIMPLEQ_FOREACH(aci, &ns->acl, entry) {
-                       if (aci_matches(aci, conn, ns, dn, rights, scope)) {
+                       if (aci_matches(aci, conn, ns, dn, rights,
+                           attr, scope)) {
                                type = aci->type;
-                               log_debug("%s by: %s %02X access to %s by %s",
+                               log_debug("%s by: %s %02X access to %s%s%s by 
%s",
                                    type == ACI_ALLOW ? "allowed" : "denied",
                                    aci->type == ACI_ALLOW ? "allow" : "deny",
                                    aci->rights,
                                    aci->target ? aci->target : "any",
+                                   aci->attribute ? " attribute " : "",
+                                   aci->attribute ? aci->attribute : "",
                                    aci->subject ? aci->subject : "any");
                        }
                }
@@ -319,7 +334,7 @@ ldap_auth_simple(struct request *req, ch
                return LDAP_INVALID_CREDENTIALS;
        } else {
                if (!authorized(req->conn, ns, ACI_BIND, binddn,
-                   LDAP_SCOPE_BASE))
+                   NULL, LDAP_SCOPE_BASE))
                        return LDAP_INSUFFICIENT_ACCESS;
 
                elm = namespace_get(ns, binddn);
Index: usr.sbin/ldapd/ldapd.conf.5
===================================================================
RCS file: /cvs/src/usr.sbin/ldapd/ldapd.conf.5,v
retrieving revision 1.22
diff -u -p -u -p -r1.22 ldapd.conf.5
--- usr.sbin/ldapd/ldapd.conf.5 17 Oct 2016 14:03:17 -0000      1.22
+++ usr.sbin/ldapd/ldapd.conf.5 11 May 2018 14:09:01 -0000
@@ -248,6 +248,13 @@ This is the default if no scope is speci
 The filter rule applies to the root DSE.
 .El
 .Pp
+The scope scope can be restricted to an optional attribute:
+.Bl -tag -width Ds
+.It attribute Ar name
+The filter rule applies to the specified attribute.
+Attributes can only be specified for write access rules.
+.El
+.Pp
 Finally, the filter rule can match a bind DN:
 .Bl -tag -width Ds
 .It by any
Index: usr.sbin/ldapd/ldapd.h
===================================================================
RCS file: /cvs/src/usr.sbin/ldapd/ldapd.h,v
retrieving revision 1.28
diff -u -p -u -p -r1.28 ldapd.h
--- usr.sbin/ldapd/ldapd.h      24 Feb 2017 14:28:31 -0000      1.28
+++ usr.sbin/ldapd/ldapd.h      11 May 2018 14:09:01 -0000
@@ -461,7 +461,7 @@ extern struct imsgev        *iev_ldapd;
 int                     ldap_bind(struct request *req);
 void                    ldap_bind_continue(struct conn *conn, int ok);
 int                     authorized(struct conn *conn, struct namespace *ns,
-                               int rights, char *dn, int scope);
+                               int rights, char *dn, char *attr, int scope);
 
 /* parse.y */
 int                     parse_config(char *filename);
Index: usr.sbin/ldapd/modify.c
===================================================================
RCS file: /cvs/src/usr.sbin/ldapd/modify.c,v
retrieving revision 1.20
diff -u -p -u -p -r1.20 modify.c
--- usr.sbin/ldapd/modify.c     28 Jul 2017 12:58:52 -0000      1.20
+++ usr.sbin/ldapd/modify.c     11 May 2018 14:09:01 -0000
@@ -32,10 +32,11 @@ int
 ldap_delete(struct request *req)
 {
        struct btval             key;
-       char                    *dn;
+       char                    *dn, *s;
        struct namespace        *ns;
        struct referrals        *refs;
        struct cursor           *cursor;
+       struct ber_element      *entry, *elm, *a;
        int                      rc = LDAP_OTHER;
 
        ++stats.req_mod;
@@ -54,7 +55,7 @@ ldap_delete(struct request *req)
                        return ldap_refer(req, dn, NULL, refs);
        }
 
-       if (!authorized(req->conn, ns, ACI_WRITE, dn, LDAP_SCOPE_BASE))
+       if (!authorized(req->conn, ns, ACI_WRITE, dn, NULL, LDAP_SCOPE_BASE))
                return ldap_respond(req, LDAP_INSUFFICIENT_ACCESS);
 
        if (namespace_begin(ns) != 0) {
@@ -91,6 +92,24 @@ ldap_delete(struct request *req)
                goto done;
        }
 
+       if ((entry = namespace_get(ns, dn)) == NULL) {
+               rc = LDAP_NO_SUCH_OBJECT;
+               goto done;
+       }
+
+       /* Fail if this leaf node includes non-writeable attributes */
+       if (entry->be_encoding != BER_TYPE_SEQUENCE)
+               goto done;
+       for (elm = entry->be_sub; elm != NULL; elm = elm->be_next) {
+               a = elm->be_sub;
+               if (a && ber_get_string(a, &s) == 0 &&
+                   !authorized(req->conn, ns, ACI_WRITE, dn, s,
+                   LDAP_SCOPE_BASE)) {
+                       rc = LDAP_INSUFFICIENT_ACCESS;
+                       goto done;
+               }
+       }
+
        if (namespace_del(ns, dn) == 0 && namespace_commit(ns) == 0)
                rc = LDAP_SUCCESS;
 
@@ -132,7 +151,7 @@ ldap_add(struct request *req)
                        return ldap_refer(req, dn, NULL, refs);
        }
 
-       if (!authorized(req->conn, ns, ACI_WRITE, dn, LDAP_SCOPE_BASE))
+       if (!authorized(req->conn, ns, ACI_WRITE, dn, NULL, LDAP_SCOPE_BASE))
                return ldap_respond(req, LDAP_INSUFFICIENT_ACCESS);
 
        /* Check that we're not adding immutable attributes.
@@ -141,6 +160,9 @@ ldap_add(struct request *req)
                attr = elm->be_sub;
                if (attr == NULL || ber_get_string(attr, &s) != 0)
                        return ldap_respond(req, LDAP_PROTOCOL_ERROR);
+               if (!authorized(req->conn, ns, ACI_WRITE, dn, s,
+                   LDAP_SCOPE_BASE))
+                       return ldap_respond(req, LDAP_INSUFFICIENT_ACCESS);
                if (!ns->relax) {
                        at = lookup_attribute(conf->schema, s);
                        if (at == NULL) {
@@ -242,8 +264,14 @@ ldap_modify(struct request *req)
                        return ldap_refer(req, dn, NULL, refs);
        }
 
-       if (!authorized(req->conn, ns, ACI_WRITE, dn, LDAP_SCOPE_BASE))
-               return ldap_respond(req, LDAP_INSUFFICIENT_ACCESS);
+       /* Check authorization for each mod to consider attributes */
+       for (mod = mods->be_sub; mod; mod = mod->be_next) {
+               if (ber_scanf_elements(mod, "{E{es", &op, &prev, &attr) != 0)
+                       return ldap_respond(req, LDAP_PROTOCOL_ERROR);
+               if (!authorized(req->conn, ns, ACI_WRITE, dn, attr,
+                   LDAP_SCOPE_BASE))
+                       return ldap_respond(req, LDAP_INSUFFICIENT_ACCESS);
+       }
 
        if (namespace_begin(ns) == -1) {
                if (errno == EBUSY) {
Index: usr.sbin/ldapd/parse.y
===================================================================
RCS file: /cvs/src/usr.sbin/ldapd/parse.y,v
retrieving revision 1.26
diff -u -p -u -p -r1.26 parse.y
--- usr.sbin/ldapd/parse.y      26 Apr 2018 14:12:19 -0000      1.26
+++ usr.sbin/ldapd/parse.y      11 May 2018 14:09:02 -0000
@@ -96,7 +96,7 @@ struct ldapd_config   *conf;
 SPLAY_GENERATE(ssltree, ssl, ssl_nodes, ssl_cmp);
 
 static struct aci      *mk_aci(int type, int rights, enum scope scope,
-                               char *target, char *subject);
+                               char *target, char *subject, char *attr);
 
 typedef struct {
        union {
@@ -120,7 +120,7 @@ static struct namespace *current_ns = NU
 %token  <v.number>     NUMBER
 %type  <v.number>      port ssl boolean comp_level
 %type  <v.number>      aci_type aci_access aci_rights aci_right aci_scope
-%type  <v.string>      aci_target aci_subject certname
+%type  <v.string>      aci_target aci_attr aci_subject certname
 %type  <v.aci>         aci
 
 %%
@@ -294,8 +294,8 @@ comp_level  : /* empty */                   { $$ = 6; }
                | LEVEL NUMBER                  { $$ = $2; }
                ;
 
-aci            : aci_type aci_access TO aci_scope aci_target aci_subject {
-                       if (($$ = mk_aci($1, $2, $4, $5, $6)) == NULL) {
+aci            : aci_type aci_access TO aci_scope aci_target aci_attr 
aci_subject {
+                       if (($$ = mk_aci($1, $2, $4, $5, $6, $7)) == NULL) {
                                free($5);
                                free($6);
                                YYERROR;
@@ -303,7 +303,7 @@ aci         : aci_type aci_access TO aci_scope 
                }
                | aci_type aci_access {
                        if (($$ = mk_aci($1, $2, LDAP_SCOPE_SUBTREE, NULL,
-                           NULL)) == NULL) {
+                           NULL, NULL)) == NULL) {
                                YYERROR;
                        }
                }
@@ -338,6 +338,10 @@ aci_target : ANY                           { $$ = NULL; }
                | STRING                        { $$ = $1; normalize_dn($$); }
                ;
 
+aci_attr       : /* empty */                   { $$ = NULL; }
+               | ATTRIBUTE STRING              { $$ = $2; }
+               ;
+
 aci_subject    : /* empty */                   { $$ = NULL; }
                | BY ANY                        { $$ = NULL; }
                | BY STRING                     { $$ = $2; normalize_dn($$); }
@@ -425,6 +429,7 @@ lookup(char *s)
                { "access",             ACCESS },
                { "allow",              ALLOW },
                { "any",                ANY },
+               { "attribute",          ATTRIBUTE },
                { "bind",               BIND },
                { "by",                 BY },
                { "cache-size",         CACHE_SIZE },
@@ -1134,7 +1139,8 @@ interface(const char *s, const char *cer
 }
 
 static struct aci *
-mk_aci(int type, int rights, enum scope scope, char *target, char *subject)
+mk_aci(int type, int rights, enum scope scope, char *target, char *attr,
+    char *subject)
 {
        struct aci      *aci;
 
@@ -1146,14 +1152,23 @@ mk_aci(int type, int rights, enum scope 
        aci->rights = rights;
        aci->scope = scope;
        aci->target = target;
+       aci->attribute = attr;
        aci->subject = subject;
 
-       log_debug("%s %02X access to %s scope %d by %s",
+       log_debug("%s %02X access to %s%s%s scope %d by %s",
            aci->type == ACI_DENY ? "deny" : "allow",
            aci->rights,
            aci->target ? aci->target : "any",
+           aci->attribute ? " attribute " : "",
+           aci->attribute ? aci->attribute : "",
            aci->scope,
            aci->subject ? aci->subject : "any");
+
+       if (aci->attribute && aci->rights != ACI_WRITE) {
+               yyerror("attributes only supported for write access filters");
+               free(aci);
+               return NULL;
+       }
 
        return aci;
 }
Index: usr.sbin/ldapd/search.c
===================================================================
RCS file: /cvs/src/usr.sbin/ldapd/search.c,v
retrieving revision 1.18
diff -u -p -u -p -r1.18 search.c
--- usr.sbin/ldapd/search.c     20 Jan 2017 11:55:08 -0000      1.18
+++ usr.sbin/ldapd/search.c     11 May 2018 14:09:02 -0000
@@ -222,7 +222,7 @@ check_search_entry(struct btval *key, st
        }
 
        if (!authorized(search->conn, search->ns, ACI_READ, dn0,
-           LDAP_SCOPE_BASE)) {
+           NULL, LDAP_SCOPE_BASE)) {
                /* LDAP_INSUFFICIENT_ACCESS */
                free(dn0);
                return 0;
@@ -880,7 +880,7 @@ ldap_search(struct request *req)
        if (*search->basedn == '\0') {
                /* request for the root DSE */
                if (!authorized(req->conn, NULL, ACI_READ, "",
-                   LDAP_SCOPE_BASE)) {
+                   NULL, LDAP_SCOPE_BASE)) {
                        reason = LDAP_INSUFFICIENT_ACCESS;
                        goto done;
                }
@@ -897,7 +897,7 @@ ldap_search(struct request *req)
        if (strcasecmp(search->basedn, "cn=schema") == 0) {
                /* request for the subschema subentries */
                if (!authorized(req->conn, NULL, ACI_READ,
-                   "cn=schema", LDAP_SCOPE_BASE)) {
+                   "cn=schema", NULL, LDAP_SCOPE_BASE)) {
                        reason = LDAP_INSUFFICIENT_ACCESS;
                        goto done;
                }
@@ -926,7 +926,7 @@ ldap_search(struct request *req)
        }
 
        if (!authorized(req->conn, search->ns, ACI_READ,
-           search->basedn, search->scope)) {
+           search->basedn, NULL, search->scope)) {
                reason = LDAP_INSUFFICIENT_ACCESS;
                goto done;
        }

Reply via email to