On Fri, May 11, 2018 at 10:42:32PM +0200, Reyk Floeter wrote:
> 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?
ok. Read filters would be good to have, but this is a nice addition as it is.
>
> 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;
> }
>