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; }