From: Nick Porter Date: Thu, 13 May 2021 14:25:09 +0000 (+0100) Subject: v4: Add prepend operator for inserting attributes at the beginning of lists (#3673) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=936b8a22e74d8d8140f4b6cddb1cf7bbdaaac1f3;p=thirdparty%2Ffreeradius-server.git v4: Add prepend operator for inserting attributes at the beginning of lists (#3673) * Add prepend operator ^= To be used in attribute assignement to get the new attribute prepended to the list Beneficial in DHCP where the order of multiple instances of an attribute is significant * Add fr_dlist_move_head() to insert one list at the head of another * Add fr_pair_list_prepend to insert one list at the head of another * Add "prepend" functionality to radius_pairmove radis_pairmove is used by rlm_files for adding attributes to the reply list. This allows for syntax such as: ^= which will insert = at the top of the reply list. Intended for DHCP use where, for example, the sequence that search domains are returned is significant. * Allow parsing of ^= operator in config. * Use fr_pair_list_prepend in map_to_request * Add an operator parameter to fr_pair_list_move If the passed in operator is T_OP_PREPEND then any added attributes are prepended to the "to" list rather than appended. Existing calls to fr_pair_list_move are updated to call with T_OP_ADD to preserve existing behaviour. * Add prepend handling for whole list copying Allows for update { &reply: ^= &request } which would insert all entry from request at the beginning of reply * Add test for prepend operator * Allow prepend operator for CSV maps * Add prepend operator tests to CSV and SQL maps * Update documentation to include ^= operator * Correct rlm_rest documentation to reflect code * Test adding whole array in json map using both += and ^= operators * Return different operators for rlm_rest module to process * Check different operators are respected by rlm_rest * Test use of prepend operator in json map from rest xlat --- diff --git a/doc/antora/modules/raddb/pages/mods-available/csv.adoc b/doc/antora/modules/raddb/pages/mods-available/csv.adoc index 3d6612866af..3d0b5b987d2 100644 --- a/doc/antora/modules/raddb/pages/mods-available/csv.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/csv.adoc @@ -146,7 +146,7 @@ Where: | Parameter | Description | | Is the destination RADIUS attribute with any valid list and request qualifiers. -| | Is any assignment attribute (=, :=, +=, -=). +| | Is any assignment attribute (=, :=, +=, ^=, -=). | | Is the name of a field from the CSV file, as taken from the `fields` configuration item. |=== diff --git a/doc/antora/modules/raddb/pages/mods-available/json.adoc b/doc/antora/modules/raddb/pages/mods-available/json.adoc index 081ac46c2b8..ff2d7c4d5da 100644 --- a/doc/antora/modules/raddb/pages/mods-available/json.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/json.adoc @@ -34,10 +34,12 @@ Selectors currently implemented are: * Automatic casting will occur between JSON and attribute types where possible. * Assignment of JSON objects/arrays to strings is supported, in which case the JSON serialized form of the object/array is used. - * If a jpath matches multiple nodes, unless the map includes the `+=` operator - * only the first node's value will be used. - * If the map uses `+=` then multiple instances of the attribute will be created, - each holding a different node value. + * If a jpath matches multiple nodes, unless the map includes the `+=` or `^=` + operator only the first node's value will be used. + * If the map uses `+=` or `^=` then multiple instances of the attribute will be + created, each holding a different node value. + * If the map uses `^=` then any new instances will be inserted at the head of + of the list. ==== diff --git a/doc/antora/modules/raddb/pages/mods-available/ldap.adoc b/doc/antora/modules/raddb/pages/mods-available/ldap.adoc index 930e834536e..9d04aa6e57d 100644 --- a/doc/antora/modules/raddb/pages/mods-available/ldap.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/ldap.adoc @@ -125,7 +125,7 @@ Where: | Parameter | Description | | Is the attribute you wish to create, with any valid list and request qualifiers. -| | Is any assignment operator (`=`, `:=`, `+=`, `-=`). +| | Is any assignment operator (`=`, `:=`, `+=`, `^=`, `-=`). | | Is the value to parse into the new attribute. If the value is wrapped in double quotes it will be xlat expanded. @@ -163,7 +163,7 @@ Where: | Parameter | Description | | Is the destination RADIUS attribute with any valid list and request qualifiers. -| | Is any assignment attribute (=, :=, +=, -=). +| | Is any assignment attribute (=, :=, +=, ^=, -=). | | Is the attribute associated with user or profile objects in the LDAP directory. If the attribute name is wrapped in double quotes diff --git a/doc/antora/modules/raddb/pages/mods-available/rest.adoc b/doc/antora/modules/raddb/pages/mods-available/rest.adoc index 396d4458299..69fc6fa34d0 100644 --- a/doc/antora/modules/raddb/pages/mods-available/rest.adoc +++ b/doc/antora/modules/raddb/pages/mods-available/rest.adoc @@ -247,10 +247,7 @@ The response format adds three optional fields: | `is_json` | If `true`, any nested JSON data will be copied to the attribute in string form. Defaults to `true`. | `op` | Controls how the attribute is inserted into the target list. - Defaults to `:=`. To create multiple attributes from multiple - values, this should be set to `+=`, otherwise only the last - value will be used, and it will be assigned to a single - attribute. + Defaults to `:=`. |=== [source,json] diff --git a/doc/antora/modules/reference/pages/unlang/update.adoc b/doc/antora/modules/reference/pages/unlang/update.adoc index a1067f1980f..856b669c6ce 100644 --- a/doc/antora/modules/reference/pages/unlang/update.adoc +++ b/doc/antora/modules/reference/pages/unlang/update.adoc @@ -81,6 +81,8 @@ name is already present in that list, its value is replaced with the value of the current attribute. | += | Add the attribute to the tail of the list, even if attributes of the same name are already present in the list. +| ^= | Add the attribute to the head of the list, even if attributes +of the same name are already present in the list. | -= | Remove all attributes from the list that match __. | !* | Delete all occurances of the attribute, no matter what the value. |===== diff --git a/man/man5/unlang.5 b/man/man5/unlang.5 index 45c2bb0f96c..58d22cc9a14 100644 --- a/man/man5/unlang.5 +++ b/man/man5/unlang.5 @@ -1046,6 +1046,11 @@ Add the attribute to the tail of the list, even if attributes of the same name are already present in the list. When the right hand side of the expression resolves to multiple values, it means add all values to the tail of the list. +.IP ^= +Add the attribute to the head of the list, even if attributes of the +same name are already present in the list. When the right hand side +of the expression resolves to multiple values, it means prepend all +values to the head of the list. .RE .PP Filtering Operators diff --git a/man/man5/users.5 b/man/man5/users.5 index 73593fe54ae..8db266566c0 100644 --- a/man/man5/users.5 +++ b/man/man5/users.5 @@ -94,10 +94,18 @@ Not allowed as a reply item. .TP 0.5i .B "Attribute += Value" Always matches as a check item, and adds the current attribute with -value to the list of configuration items. +value to the tail of the list of configuration items. .br As a reply item, it has an identical meaning, but the attribute is -added to the reply items. +added to the tail of the reply items list. + +.TP 0.5i +.B "Attribute ^= Value" +Always matches as a check item, and adds the current attribute with +value to the head of the list of configuration items. +.br +As a reply item, it has an identical meaning, but the attribute is +added to the head of the reply items list. .TP 0.5i .B "Attribute != Value" diff --git a/scripts/ci/openresty/json-api.lua b/scripts/ci/openresty/json-api.lua index 5b252992641..add2da15a60 100644 --- a/scripts/ci/openresty/json-api.lua +++ b/scripts/ci/openresty/json-api.lua @@ -99,9 +99,13 @@ Api.endpoint('POST', '/user//mac/', value = keyData.username } returnData["control.NAS-IP-Address"] = { - op = ":=", + op = "+=", value = body.NAS or body['NAS-IP-Address'].value } + returnData["control.Tmp-String-2"] = { + op = "^=", + value = keyData.username + } return ngx.say(cjson.encode(returnData)) end ) @@ -119,6 +123,10 @@ Api.endpoint('GET', '/user//mac/', op = ":=", value = keyData.username } + returnData["control.Tmp-String-2"] = { + op = "^=", + value = keyData.username + } return ngx.say(cjson.encode(returnData)) end ) diff --git a/src/bin/radsniff.c b/src/bin/radsniff.c index d62668dfa7d..b2f3f69eb6d 100644 --- a/src/bin/radsniff.c +++ b/src/bin/radsniff.c @@ -1477,7 +1477,7 @@ static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkt * The delay is so we can detect retransmissions. */ original->linked = talloc_steal(original, packet); - fr_pair_list_move(&original->link_vps, &decoded); /* Move the vps over */ + fr_pair_list_move(&original->link_vps, &decoded, T_OP_ADD); /* Move the vps over */ rs_tv_add_ms(&header->ts, conf->stats.timeout, &original->when); if (fr_event_timer_at(NULL, event->list, &original->event, fr_time_from_timeval(&original->when), _rs_event, original) < 0) { @@ -1670,7 +1670,7 @@ static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkt fr_pair_list_free(&original->packet_vps); fr_radius_packet_free(&original->packet); original->packet = talloc_steal(original, packet); - fr_pair_list_move(&original->packet_vps, &decoded); + fr_pair_list_move(&original->packet_vps, &decoded, T_OP_ADD); /* Request may need to be reinserted as the 5 tuple of the response may of changed */ if (rs_packet_cmp(original, &search) != 0) { @@ -1681,7 +1681,7 @@ static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkt fr_pair_list_free(&original->expect_vps); fr_radius_packet_free(&original->expect); original->expect = talloc_steal(original, search.expect); - fr_pair_list_move(&original->expect_vps, &search.expect_vps); + fr_pair_list_move(&original->expect_vps, &search.expect_vps, T_OP_ADD); /* Disarm the timer for the cleanup event for the original request */ fr_event_timer_delete(&original->event); @@ -1698,10 +1698,10 @@ static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkt original->capture_p = original->capture; original->packet = talloc_steal(original, packet); - fr_pair_list_move(&original->packet_vps, &decoded); + fr_pair_list_move(&original->packet_vps, &decoded, T_OP_ADD); original->expect = talloc_steal(original, search.expect); - fr_pair_list_move(&original->expect_vps, &search.expect_vps); + fr_pair_list_move(&original->expect_vps, &search.expect_vps, T_OP_ADD); if (!fr_pair_list_empty(&search.link_vps)) { bool ret; @@ -1712,7 +1712,7 @@ static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkt vp = fr_pair_list_next(&search.link_vps, vp)) { fr_pair_steal(original, vp); } - fr_pair_list_move(&original->link_vps, &search.link_vps); + fr_pair_list_move(&original->link_vps, &search.link_vps, T_OP_ADD); /* We should never have conflicts */ ret = fr_rb_insert(link_tree, original); diff --git a/src/lib/server/cf_file.c b/src/lib/server/cf_file.c index 7a05229af30..5f343e69ce5 100644 --- a/src/lib/server/cf_file.c +++ b/src/lib/server/cf_file.c @@ -1782,7 +1782,7 @@ static int parse_input(cf_stack_t *stack) * so we check for them first. */ if (!((*ptr == '=') || (*ptr == '!') || (*ptr == '>') || (*ptr == '<') || - (*ptr == '-') || (*ptr == '+') || (*ptr == ':'))) { + (*ptr == '-') || (*ptr == '+') || (*ptr == ':') || (*ptr == '^'))) { ERROR("%s[%d]: Parse error at unexpected text: %s", frame->filename, frame->lineno, ptr); return -1; @@ -1812,6 +1812,7 @@ static int parse_input(cf_stack_t *stack) case T_OP_EQ: case T_OP_SET: + case T_OP_PREPEND: fr_skip_whitespace(ptr); op_token = name2_token; break; diff --git a/src/lib/server/map.c b/src/lib/server/map.c index 3530aa5290c..9f45f5fa69b 100644 --- a/src/lib/server/map.c +++ b/src/lib/server/map.c @@ -1335,11 +1335,16 @@ int map_to_request(request_t *request, map_t const *map, radius_map_getvalue_t f FALL_THROUGH; case T_OP_ADD: - fr_pair_list_move(list, &src_list); + fr_pair_list_move(list, &src_list, T_OP_ADD); fr_pair_list_free(&src_list); } goto update; + case T_OP_PREPEND: + fr_pair_list_move(list, &src_list, map->op); + fr_pair_list_free(&src_list); + goto update; + default: fr_pair_list_free(&src_list); rcode = -1; @@ -1557,6 +1562,14 @@ int map_to_request(request_t *request, map_t const *map, radius_map_getvalue_t f } break; + /* + * ^= - Prepend src_list attributes to the destination + */ + case T_OP_PREPEND: + fr_pair_list_prepend(list, &src_list); + fr_pair_list_free(&src_list); + break; + /* * += - Add all src_list attributes to the destination */ diff --git a/src/lib/server/map_async.c b/src/lib/server/map_async.c index f4c7c54447c..121d2392b59 100644 --- a/src/lib/server/map_async.c +++ b/src/lib/server/map_async.c @@ -1084,6 +1084,19 @@ int map_list_mod_apply(request_t *request, vp_list_mod_t const *vlm) } goto finish; + case T_OP_PREPEND: + { + fr_pair_list_t vp_from; + + fr_pair_list_init(&vp_from); + map_list_mod_to_vps(parent, &vp_from, vlm); + fr_assert(!fr_pair_list_empty(&vp_from)); + + fr_pair_list_prepend(vp_list, &vp_from); + + goto finish; + } + default: rcode = -1; goto finish; @@ -1201,6 +1214,19 @@ int map_list_mod_apply(request_t *request, vp_list_mod_t const *vlm) } goto finish; + case T_OP_PREPEND: + { + fr_pair_list_t vp_from; + + fr_pair_list_init(&vp_from); + map_list_mod_to_vps(parent, &vp_from, vlm); + fr_assert(!fr_pair_list_empty(&vp_from)); + + fr_pair_list_prepend(vp_list, &vp_from); + + goto finish; + } + /* * = - Set only if not already set */ diff --git a/src/lib/server/pairmove.c b/src/lib/server/pairmove.c index cd14c7aa14a..55bd9915673 100644 --- a/src/lib/server/pairmove.c +++ b/src/lib/server/pairmove.c @@ -46,7 +46,7 @@ void radius_pairmove(request_t *request, fr_pair_list_t *to, fr_pair_list_t *fro { int i, j, count, to_count, tailto; fr_pair_t *from_vp, *next_from, *to_vp, *next_to = NULL; - fr_pair_list_t append; + fr_pair_list_t append, prepend; bool *edited = NULL; bool *deleted = NULL; @@ -74,15 +74,16 @@ void radius_pairmove(request_t *request, fr_pair_list_t *to, fr_pair_list_t *fro */ fr_pair_list_init(&append); + fr_pair_list_init(&prepend); - to_count = fr_dlist_num_elements(&to->head); + to_count = fr_pair_list_len(to); tailto = to_count; edited = talloc_zero_array(request, bool, to_count); deleted = talloc_zero_array(request, bool, to_count); - count = to_count + fr_dlist_num_elements(&from->head); + count = to_count + fr_pair_list_len(from); - RDEBUG4("::: FROM %ld TO %d MAX %d", fr_dlist_num_elements(&from->head), to_count, count); + RDEBUG4("::: FROM %ld TO %d MAX %d", fr_pair_list_len(from), to_count, count); /* * Now that we have the lists initialized, start working @@ -104,6 +105,18 @@ void radius_pairmove(request_t *request, fr_pair_list_t *to, fr_pair_list_t *fro */ if (from_vp->op == T_OP_ADD) goto do_append; + /* + * The attribute needs to be prepended to the "to" + * list - store it in the prepend list + */ + + if (from_vp->op == T_OP_PREPEND) { + RDEBUG4("::: PREPENDING %s FROM %d", from_vp->da->name, i); + fr_pair_remove(from, from_vp); + fr_pair_prepend(&prepend, from_vp); + from_vp->op = T_OP_EQ; + continue; + } found = false; j = 0; for (to_vp = fr_pair_list_head(to); to_vp; to_vp = next_to, j++) { @@ -295,6 +308,12 @@ void radius_pairmove(request_t *request, fr_pair_list_t *to, fr_pair_list_t *fro to_vp->op = T_OP_EQ; } + /* + * Now prepend any items in the "prepend" list to + * the head of the "to" list. + */ + fr_pair_list_prepend(to, &prepend); + /* * And finally add in the attributes we're appending to * the tail of the "to" list. diff --git a/src/lib/unlang/compile.c b/src/lib/unlang/compile.c index e2fefa226cc..0e661c17c59 100644 --- a/src/lib/unlang/compile.c +++ b/src/lib/unlang/compile.c @@ -870,7 +870,7 @@ int unlang_fixup_update(map_t *map, UNUSED void *ctx) } /* - * Only += and :=, and !* operators are supported + * Only += and :=, and !*, and ^= operators are supported * for lists. */ switch (map->op) { @@ -904,6 +904,14 @@ int unlang_fixup_update(map_t *map, UNUSED void *ctx) } break; + case T_OP_PREPEND: + if (!tmpl_is_list(map->rhs) && + !tmpl_is_exec(map->rhs)) { + cf_log_err(map->ci, "Invalid source for list assignment '%s ^= ...'", map->lhs->name); + return -1; + } + break; + default: cf_log_err(map->ci, "Operator \"%s\" not allowed for list assignment", fr_table_str_by_value(fr_tokens_table, map->op, "")); diff --git a/src/lib/util/dlist.h b/src/lib/util/dlist.h index 08b63f9c09f..6dac2c11450 100644 --- a/src/lib/util/dlist.h +++ b/src/lib/util/dlist.h @@ -645,7 +645,7 @@ static inline CC_HINT(nonnull) void fr_dlist_verify(char const *file, int line, # define FR_DLIST_VERIFY(_head) #endif -/** Merge two lists, inserting the tail of one into the other +/** Merge two lists, inserting the source at the tail of the destination * */ static inline CC_HINT(nonnull) void fr_dlist_move(fr_dlist_head_t *list_dst, fr_dlist_head_t *list_src) @@ -687,6 +687,43 @@ static inline CC_HINT(nonnull) void fr_dlist_move(fr_dlist_head_t *list_dst, fr_ list_src->num_elements = 0; } +/** Merge two lists, inserting the source at the head of the destination + * + */ +static inline CC_HINT(nonnull) void fr_dlist_move_head(fr_dlist_head_t *list_dst, fr_dlist_head_t *list_src) +{ + fr_dlist_t *dst = &(list_dst->entry); + fr_dlist_t *src = &(list_src->entry); + +#ifdef WITH_VERIFY_PTR + /* + * Must be both talloced or both not + */ + if (!fr_cond_assert((list_dst->type && list_src->type) || (!list_dst->type && !list_src->type))) return; + + /* + * Must be of the same type + */ + if (!fr_cond_assert(!list_dst->type || (strcmp(list_dst->type, list_src->type) == 0))) return; +#endif + + if (!fr_cond_assert(dst->next != NULL)) return; + if (!fr_cond_assert(dst->prev != NULL)) return; + + if (fr_dlist_empty(list_src)) return; + + src->next->prev = dst; + src->prev->next = dst->next; + + dst->next->prev = src->prev; + dst->next = src->next; + + list_dst->num_elements += list_src->num_elements; + + fr_dlist_entry_init(src); + list_src->num_elements = 0; +} + /** Free the first item in the list * * @param[in] list_head to free head item in. diff --git a/src/lib/util/dpair_legacy.c b/src/lib/util/dpair_legacy.c new file mode 100644 index 00000000000..c34beb46ca8 --- /dev/null +++ b/src/lib/util/dpair_legacy.c @@ -0,0 +1,765 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** AVP manipulation and search API + * + * @file src/lib/util/pair.c + * + * @copyright 2000,2006,2015 The FreeRADIUS server project + */ +RCSID("$Id$") + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +fr_sbuff_term_t const bareword_terminals = + FR_SBUFF_TERMS( + L("\t"), + L("\n"), + L(" "), + L("!*"), + L("!="), + L("!~"), + L("&&"), /* Logical operator */ + L(")"), /* Close condition/sub-condition */ + L("+="), + L("-="), + L(":="), + L("<"), + L("<="), + L("=*"), + L("=="), + L("=~"), + L(">"), + L(">="), + L("||"), /* Logical operator */ + ); + +/** Mark a valuepair for xlat expansion + * + * Copies xlat source (unprocessed) string to valuepair value, and sets value type. + * + * @param vp to mark for expansion. + * @param value to expand. + * @return + * - 0 if marking succeeded. + * - -1 if #fr_pair_t already had a value, or OOM. + */ +int fr_pair_mark_xlat(fr_pair_t *vp, char const *value) +{ + char *raw; + + /* + * valuepair should not already have a value. + */ + if (vp->type != VT_NONE) { + fr_strerror_const("Pair already has a value"); + return -1; + } + + raw = talloc_typed_strdup(vp, value); + if (!raw) { + fr_strerror_const("Out of memory"); + return -1; + } + + vp->type = VT_XLAT; + vp->xlat = raw; + vp->vp_length = 0; + + return 0; +} + +/** Create a valuepair from an ASCII attribute and value + * + * Where the attribute name is in the form: + * - Attr-%d + * - Attr-%d.%d.%d... + * + * @param ctx for talloc + * @param dict to user for partial resolution. + * @param attribute name to parse. + * @param value to parse (must be a hex string). + * @param op to assign to new valuepair. + * @return new #fr_pair_t or NULL on error. + */ +static fr_pair_t *fr_pair_make_unknown(TALLOC_CTX *ctx, fr_dict_t const *dict, + char const *attribute, char const *value, + fr_token_t op) +{ + fr_pair_t *vp; + fr_dict_attr_t *n; + fr_sbuff_t sbuff = FR_SBUFF_IN(attribute, strlen(attribute)); + + vp = fr_pair_alloc_null(ctx); + if (!vp) return NULL; + + if ((fr_dict_unknown_afrom_oid_substr(vp, NULL, &n, fr_dict_root(dict), &sbuff, NULL) <= 0) || + fr_sbuff_remaining(&sbuff)) { + talloc_free(vp); + return NULL; + } + vp->da = n; + + /* + * No value, but ensure that we still set up vp->data properly. + */ + if (!value) { + value = ""; + + } else if (strncasecmp(value, "0x", 2) != 0) { + /* + * Unknown attributes MUST be of type 'octets' + */ + fr_strerror_printf("Unknown attribute \"%s\" requires a hex " + "string, not \"%s\"", attribute, value); + talloc_free(vp); + return NULL; + } + + if (fr_pair_value_from_str(vp, value, -1, '"', false) < 0) { + talloc_free(vp); + return NULL; + } + + vp->op = (op == 0) ? T_OP_EQ : op; + return vp; +} + +/** Create a #fr_pair_t from ASCII strings + * + * Converts an attribute string identifier (with an optional tag qualifier) + * and value string into a #fr_pair_t. + * + * The string value is parsed according to the type of #fr_pair_t being created. + * + * @param[in] ctx for talloc. + * @param[in] dict to look attributes up in. + * @param[in] vps list where the attribute will be added (optional) + * @param[in] attribute name. + * @param[in] value attribute value (may be NULL if value will be set later). + * @param[in] op to assign to new #fr_pair_t. + * @return a new #fr_pair_t. + */ +fr_pair_t *fr_pair_make(TALLOC_CTX *ctx, fr_dict_t const *dict, fr_pair_list_t *vps, + char const *attribute, char const *value, fr_token_t op) +{ + fr_dict_attr_t const *da; + fr_pair_t *vp; + char const *attrname = attribute; + + /* + * It's not found in the dictionary, so we use + * another method to create the attribute. + */ + da = fr_dict_attr_search_by_qualified_oid(NULL, dict, attrname, true); + if (!da) { + vp = fr_pair_make_unknown(ctx, dict, attrname, value, op); + if (!vp) return NULL; + + if (vps) fr_pair_add(vps, vp); + return vp; + } + + if (da->type == FR_TYPE_GROUP) { + fr_strerror_const("Attributes of type 'group' are not supported"); + return NULL; + } + + vp = fr_pair_afrom_da(ctx, da); + if (!vp) return NULL; + vp->op = (op == 0) ? T_OP_EQ : op; + + switch (vp->op) { + case T_OP_CMP_TRUE: + case T_OP_CMP_FALSE: + fr_pair_value_clear(vp); + value = NULL; /* ignore it! */ + break; + + /* + * Regular expression comparison of integer attributes + * does a STRING comparison of the names of their + * integer attributes. + */ + case T_OP_REG_EQ: /* =~ */ + case T_OP_REG_NE: /* !~ */ + { +#ifndef HAVE_REGEX + fr_strerror_const("Regular expressions are not supported"); + return NULL; +#else + ssize_t slen; + regex_t *preg; + + /* + * Someone else will fill in the value. + */ + if (!value) break; + + talloc_free(vp); + + slen = regex_compile(ctx, &preg, value, strlen(value), NULL, false, true); + if (slen <= 0) { + fr_strerror_printf_push("Error at offset %zu compiling regex for %s", -slen, attribute); + return NULL; + } + talloc_free(preg); + + vp = fr_pair_afrom_da(ctx, da); + if (!vp) return NULL; + vp->op = op; + + if (fr_pair_mark_xlat(vp, value) < 0) { + talloc_free(vp); + return NULL; + } + + value = NULL; /* ignore it */ + break; +#endif + } + default: + break; + } + + /* + * We probably want to fix fr_pair_value_from_str to accept + * octets as values for any attribute. + */ + if (value && (fr_pair_value_from_str(vp, value, -1, '\"', true) < 0)) { + talloc_free(vp); + return NULL; + } + + if (vps) fr_pair_add(vps, vp); + return vp; +} + +/** Read one line of attribute/value pairs into a list. + * + * The line may specify multiple attributes separated by commas. + * + * @note If the function returns #T_INVALID, an error has occurred and + * @note the valuepair list should probably be freed. + * + * @param[in] ctx for talloc + * @param[in] parent parent to start referencing from + * @param[in] buffer to read valuepairs from. + * @param[in] list where the parsed fr_pair_ts will be appended. + * @param[in,out] token The last token we parsed + * @param[in] depth the nesting depth for FR_TYPE_GROUP + * @return + * - <= 0 on failure. + * - The number of bytes of name consumed on success. + */ +static ssize_t fr_pair_list_afrom_substr(TALLOC_CTX *ctx, fr_dict_attr_t const *parent, char const *buffer, + fr_pair_list_t *list, fr_token_t *token, int depth) +{ + fr_pair_list_t tmp_list; + fr_pair_t *vp; + char const *p, *next; + fr_token_t last_token = T_INVALID; + fr_pair_t_RAW raw; + fr_dict_attr_t const *internal = fr_dict_root(fr_dict_internal()); + + if (internal == parent) internal = NULL; + + /* + * We allow an empty line. + */ + if (buffer[0] == 0) { + *token = T_EOL; + return 0; + } + + fr_pair_list_init(&tmp_list); + + p = buffer; + while (true) { + ssize_t slen; + fr_dict_attr_t const *da; + fr_dict_attr_t *da_unknown = NULL; + fr_skip_whitespace(p); + + /* + * Stop at the end of the input, returning + * whatever token was last read. + */ + if (!*p) break; + + if (*p == '#') { + last_token = T_EOL; + break; + } + + /* + * Hacky hack... + */ + if (strncmp(p, "raw.", 4) == 0) goto do_unknown; + + /* + * Parse the name. + */ + slen = fr_dict_attr_by_oid_substr(NULL, &da, parent, + &FR_SBUFF_IN(p, strlen(p)), &bareword_terminals); + if ((slen <= 0) && internal) { + slen = fr_dict_attr_by_oid_substr(NULL, &da, internal, + &FR_SBUFF_IN(p, strlen(p)), &bareword_terminals); + } + if (slen <= 0) { + do_unknown: + slen = fr_dict_unknown_afrom_oid_substr(ctx, NULL, &da_unknown, parent, + &FR_SBUFF_IN(p, strlen(p)), &bareword_terminals); + if (slen <= 0) { + p += -slen; + + error: + fr_pair_list_free(&tmp_list); + *token = T_INVALID; + return -(p - buffer); + } + + da = da_unknown; + } + + next = p + slen; + + if ((size_t) (next - p) >= sizeof(raw.l_opand)) { + fr_dict_unknown_free(&da); + fr_strerror_const("Attribute name too long"); + goto error; + } + + memcpy(raw.l_opand, p, next - p); + raw.l_opand[next - p] = '\0'; + raw.r_opand[0] = '\0'; + + p = next; + fr_skip_whitespace(p); + + /* + * There must be an operator here. + */ + raw.op = gettoken(&p, raw.r_opand, sizeof(raw.r_opand), false); + if ((raw.op < T_EQSTART) || (raw.op > T_EQEND)) { + fr_dict_unknown_free(&da); + fr_strerror_const("Expecting operator"); + goto error; + } + + fr_skip_whitespace(p); + + /* + * Allow grouping attributes. + */ + if ((da->type == FR_TYPE_GROUP) || (da->type == FR_TYPE_TLV) || (da->type == FR_TYPE_STRUCT)) { + if (*p != '{') { + fr_strerror_printf("Group list for %s MUST start with '{'", da->name); + goto error; + } + p++; + + vp = fr_pair_afrom_da(ctx, da); + if (!vp) goto error; + + /* + * Find the new root attribute to start encoding from. + */ + parent = fr_dict_attr_ref(da); + if (!parent) parent = da; + + slen = fr_pair_list_afrom_substr(vp, parent, p, &vp->vp_group, &last_token, depth + 1); + if (slen <= 0) { + talloc_free(vp); + goto error; + } + + if (last_token != T_RCBRACE) { + failed_group: + fr_strerror_const("Failed to end group list with '}'"); + talloc_free(vp); + goto error; + } + + p += slen; + fr_skip_whitespace(p); + if (*p != '}') goto failed_group; + p++; + + } else { + fr_token_t quote; + char const *q; + + /* + * Get the RHS thing. + */ + quote = gettoken(&p, raw.r_opand, sizeof(raw.r_opand), false); + if (quote == T_EOL) { + fr_strerror_const("Failed to get value"); + goto error; + } + + switch (quote) { + /* + * Perhaps do xlat's + */ + case T_DOUBLE_QUOTED_STRING: + /* + * Only report as double quoted if it contained valid + * a valid xlat expansion. + */ + q = strchr(raw.r_opand, '%'); + if (q && (q[1] == '{')) { + raw.quote = quote; + } else { + raw.quote = T_SINGLE_QUOTED_STRING; + } + break; + + case T_SINGLE_QUOTED_STRING: + case T_BACK_QUOTED_STRING: + case T_BARE_WORD: + raw.quote = quote; + break; + + default: + fr_strerror_printf("Failed to find expected value on right hand side in %s", da->name); + goto error; + } + + fr_skip_whitespace(p); + + /* + * Regular expressions get sanity checked by pair_make(). + * + * @todo - note that they will also be escaped, + * so we may need to fix that later. + */ + if ((raw.op == T_OP_REG_EQ) || (raw.op == T_OP_REG_NE)) { + vp = fr_pair_afrom_da(ctx, da); + if (!vp) goto error; + vp->op = raw.op; + + fr_pair_value_bstrndup(vp, raw.r_opand, strlen(raw.r_opand), false); + } else { + /* + * All other attributes get the name + * parsed. + */ + vp = fr_pair_afrom_da(ctx, da); + if (!vp) goto error; + vp->op = raw.op; + + /* + * We don't care what the value is, so + * ignore it. + */ + if ((raw.op == T_OP_CMP_TRUE) || (raw.op == T_OP_CMP_FALSE)) goto next; + + /* + * fr_pair_raw_from_str() only returns this when + * the input looks like it needs to be xlat'd. + */ + if (raw.quote == T_DOUBLE_QUOTED_STRING) { + if (fr_pair_mark_xlat(vp, raw.r_opand) < 0) { + talloc_free(vp); + goto error; + } + + /* + * Parse it ourselves. The RHS + * might NOT be tainted, but we + * don't know. So just mark it + * as such to be safe. + */ + } else if (fr_pair_value_from_str(vp, raw.r_opand, -1, '"', true) < 0) { + talloc_free(vp); + goto error; + } + } + } + + next: + /* + * Free the unknown attribute, we don't need it any more. + */ + fr_dict_unknown_free(&da); + + fr_pair_add(&tmp_list, vp); + + /* + * Now look for EOL, hash, etc. + */ + if (!*p || (*p == '#') || (*p == '\n')) { + last_token = T_EOL; + break; + } + + /* + * Check for nested groups. + */ + if ((depth > 0) && (p[0] == ' ') && (p[1] == '}')) p++; + + /* + * Stop at '}', too, if we're inside of a group. + */ + if ((depth > 0) && (*p == '}')) { + last_token = T_RCBRACE; + break; + } + + if (*p != ',') { + fr_strerror_printf("Expected ',', got '%c' at offset %zu", *p, p - buffer); + goto error; + } + p++; + last_token = T_COMMA; + } + + if (!fr_pair_list_empty(&tmp_list)) fr_tmp_pair_list_move(list, &tmp_list); + + /* + * And return the last token which we read. + */ + *token = last_token; + return p - buffer; +} + +/** Read one line of attribute/value pairs into a list. + * + * The line may specify multiple attributes separated by commas. + * + * @note If the function returns #T_INVALID, an error has occurred and + * @note the valuepair list should probably be freed. + * + * @param[in] ctx for talloc + * @param[in] dict to resolve attributes in. + * @param[in] buffer to read valuepairs from. + * @param[in] list where the parsed fr_pair_ts will be appended. + * @return the last token parsed, or #T_INVALID + */ +fr_token_t fr_pair_list_afrom_str(TALLOC_CTX *ctx, fr_dict_t const *dict, char const *buffer, fr_pair_list_t *list) +{ + fr_token_t token; + + (void) fr_pair_list_afrom_substr(ctx, fr_dict_root(dict), buffer, list, &token, 0); + return token; +} + +/** Read valuepairs from the fp up to End-Of-File. + * + * @param[in] ctx for talloc + * @param[in] dict to resolve attributes in. + * @param[in,out] out where the parsed fr_pair_ts will be appended. + * @param[in] fp to read valuepairs from. + * @param[out] pfiledone true if file parsing complete; + * @return + * - 0 on success + * - -1 on error + */ +int fr_pair_list_afrom_file(TALLOC_CTX *ctx, fr_dict_t const *dict, fr_pair_list_t *out, FILE *fp, bool *pfiledone) +{ + fr_token_t last_token = T_EOL; + bool found = false; + char buf[8192]; + + while (fgets(buf, sizeof(buf), fp) != NULL) { + fr_pair_list_t tmp_list; + + /* + * If we get a '\n' by itself, we assume that's + * the end of that VP list. + */ + if (buf[0] == '\n') { + if (found) { + *pfiledone = false; + return 0; + } + continue; + } + + /* + * Comments get ignored + */ + if (buf[0] == '#') continue; + + /* + * Read all of the attributes on the current line. + * + * If we get nothing but an EOL, it's likely OK. + */ + fr_pair_list_init(&tmp_list); + last_token = fr_pair_list_afrom_str(ctx, dict, buf, &tmp_list); + if (fr_dlist_empty(&tmp_list.head)) { + if (last_token == T_EOL) break; + + /* + * Didn't read anything, but the previous + * line wasn't EOL. The input file has a + * format error. + */ + *pfiledone = false; + fr_pair_list_free(out); + return -1; + } + + found = true; + fr_tmp_pair_list_move(out, &tmp_list); + } + + *pfiledone = true; + return 0; +} + + +/** Move pairs from source list to destination list respecting operator + * + * @note This function does some additional magic that's probably not needed + * in most places. Consider using radius_pairmove in server code. + * + * @note fr_pair_list_free should be called on the head of the source list to free + * unmoved attributes (if they're no longer needed). + * + * @param[in,out] to destination list. + * @param[in,out] from source list. + * + * @see radius_pairmove + */ +void fr_pair_list_move(fr_pair_list_t *to, fr_pair_list_t *from, fr_token_t op) +{ + fr_pair_t *i, *found; + fr_pair_list_t head_new, head_prepend; + + if (!to || fr_pair_list_empty(from)) return; + + /* + * We're editing the "to" list while we're adding new + * attributes to it. We don't want the new attributes to + * be edited, so we create an intermediate list to hold + * them during the editing process. + */ + fr_pair_list_init(&head_new); + fr_pair_list_init(&head_prepend); + + /* + * We're looping over the "from" list, moving some + * attributes out, but leaving others in place. + */ + for (i = fr_pair_list_head(from); i; ) { + fr_pair_t *j; + + VP_VERIFY(i); + + /* + * We never move Fall-Through. + */ + if (fr_dict_attr_is_top_level(i->da) && (i->da->attr == FR_FALL_THROUGH)) { + i = fr_pair_list_next(from, i); + continue; + } + + /* + * Unlike previous versions, we treat all other + * attributes as normal. i.e. there's no special + * treatment for passwords or Hint. + */ + + switch (i->op) { + /* + * Anything else are operators which + * shouldn't occur. We ignore them, and + * leave them in place. + */ + default: + i = fr_pair_list_next(from, i); + continue; + + /* + * Add it to the "to" list, but only if + * it doesn't already exist. + */ + case T_OP_EQ: + found = fr_pair_find_by_da(to, i->da); + if (!found) goto do_add; + + i = fr_pair_list_next(from, i); + continue; + + /* + * Add it to the "to" list, and delete any attribute + * of the same vendor/attr which already exists. + */ + case T_OP_SET: + found = fr_pair_find_by_da(to, i->da); + if (!found) goto do_add; + + /* + * Delete *all* of the attributes + * of the same number. + */ + fr_pair_delete_by_da(to, found->da); + goto do_add; + + /* + * Move it from the old list and add it + * to the new list. + */ + case T_OP_ADD: + do_add: + j = fr_pair_list_next(from, i); + fr_pair_remove(from, i); + fr_pair_add(&head_new, i); + i = j; + continue; + case T_OP_PREPEND: + j = fr_pair_list_next(from, i); + fr_pair_remove(from, i); + fr_pair_prepend(&head_prepend, i); + i = j; + continue; + } + } /* loop over the "from" list. */ + + /* + * If the op parameter was prepend add the "new" list + * attributes first as those whose individual operator + * is prepend should be prepended to the resulting list + */ + if (op == T_OP_PREPEND) fr_tmp_pair_list_move_head(to, &head_new); + + /* + * If there are any items in the prepend list prepend + * it to the "to" list + */ + fr_tmp_pair_list_move_head(to, &head_prepend); + + /* + * If the op parameter was not prepend, take the "new" + * list and append it to the "to" list + */ + if (op != T_OP_PREPEND) fr_tmp_pair_list_move(to, &head_new); +} diff --git a/src/lib/util/pair.c b/src/lib/util/pair.c index b01e84d5e4f..4fed17cff2d 100644 --- a/src/lib/util/pair.c +++ b/src/lib/util/pair.c @@ -2526,6 +2526,16 @@ void fr_pair_list_append(fr_pair_list_t *dst, fr_pair_list_t *src) fr_dlist_move(&dst->head, &src->head); } +/** Move a list of fr_pair_t from a temporary list to the head of a destination list + * + * @param dst list to move pairs into + * @param src from which to take pairs + */ +void fr_pair_list_prepend(fr_pair_list_t *dst, fr_pair_list_t *src) +{ + fr_dlist_move_head(&dst->head, &src->head); +} + /** Evaluation function for matching if vp matches a given da * * Can be used as a filter function for fr_dcursor_filter_next() diff --git a/src/lib/util/pair.h b/src/lib/util/pair.h index 5a5cfb87b86..2a668b9389a 100644 --- a/src/lib/util/pair.h +++ b/src/lib/util/pair.h @@ -285,6 +285,7 @@ int fr_pair_list_copy_by_ancestor(TALLOC_CTX *ctx, fr_pair_list_t *to, fr_pair_list_t *from, fr_dict_attr_t const *parent_da, unsigned int count); int fr_pair_sublist_copy(TALLOC_CTX *ctx, fr_pair_list_t *to, fr_pair_list_t const *from, fr_pair_t *item); void fr_pair_list_append(fr_pair_list_t *dst, fr_pair_list_t *src); +void fr_pair_list_prepend(fr_pair_list_t *dst, fr_pair_list_t *src); /** @hidecallergraph */ void *fr_pair_list_head(fr_pair_list_t const *list); diff --git a/src/lib/util/pair_legacy.c b/src/lib/util/pair_legacy.c index e9662ebc997..9b62a14463b 100644 --- a/src/lib/util/pair_legacy.c +++ b/src/lib/util/pair_legacy.c @@ -720,13 +720,14 @@ int fr_pair_list_afrom_file(TALLOC_CTX *ctx, fr_dict_t const *dict, fr_pair_list * * @param[in,out] to destination list. * @param[in,out] from source list. + * @param[in] op operator for list move. * * @see radius_pairmove */ -void fr_pair_list_move(fr_pair_list_t *to, fr_pair_list_t *from) +void fr_pair_list_move(fr_pair_list_t *to, fr_pair_list_t *from, fr_token_t op) { fr_pair_t *i, *found; - fr_pair_list_t head_new; + fr_pair_list_t head_new, head_prepend; if (!to || fr_pair_list_empty(from)) return; @@ -738,6 +739,12 @@ void fr_pair_list_move(fr_pair_list_t *to, fr_pair_list_t *from) */ fr_pair_list_init(&head_new); + /* + * Any attributes that are requested to be prepended + * are added to a temporary list here + */ + fr_pair_list_init(&head_prepend); + /* * We're looping over the "from" list, moving some * attributes out, but leaving others in place. @@ -808,11 +815,32 @@ void fr_pair_list_move(fr_pair_list_t *to, fr_pair_list_t *from) fr_pair_append(&head_new, i); i = j; continue; + case T_OP_PREPEND: + j = fr_pair_list_next(from, i); + fr_pair_remove(from, i); + fr_pair_prepend(&head_prepend, i); + i = j; + continue; } } /* loop over the "from" list. */ /* - * Take the "new" list, and append it to the "to" list. + * If the op parameter was prepend, add the "new list + * attributes first as those whose individual operator + * is prepend should be prepended to the resulting list */ - fr_pair_list_append(to, &head_new); + if (op == T_OP_PREPEND) fr_pair_list_prepend(to, &head_new); + + /* + * If there are any items in the prepend list prepend + * it to the "to" list + */ + fr_pair_list_prepend(to, &head_prepend); + + /* + * If the op parameter was not prepend, take the "new" + * list, and append it to the "to" list. + */ + if (op != T_OP_PREPEND) fr_pair_list_append(to, &head_new); + } diff --git a/src/lib/util/pair_legacy.h b/src/lib/util/pair_legacy.h index db758a0aab2..62aa31781f0 100644 --- a/src/lib/util/pair_legacy.h +++ b/src/lib/util/pair_legacy.h @@ -44,7 +44,7 @@ fr_token_t fr_pair_list_afrom_str(TALLOC_CTX *ctx, fr_dict_t const *dict, int fr_pair_list_afrom_file(TALLOC_CTX *ctx, fr_dict_t const *dict, fr_pair_list_t *out, FILE *fp, bool *pfiledone); -void fr_pair_list_move(fr_pair_list_t *to, fr_pair_list_t *from); +void fr_pair_list_move(fr_pair_list_t *to, fr_pair_list_t *from, fr_token_t op); #ifdef __cplusplus } diff --git a/src/lib/util/pair_legacy_tests.c b/src/lib/util/pair_legacy_tests.c index 7264f360ecd..be40fb29097 100644 --- a/src/lib/util/pair_legacy_tests.c +++ b/src/lib/util/pair_legacy_tests.c @@ -226,8 +226,8 @@ static void test_fr_pair_list_move(void) TEST_CHECK(fr_pair_list_afrom_file(autofree, test_dict, &old_list, fp, &pfiledone) == 0); TEST_CHECK(pfiledone == true); - TEST_CASE("Move pair from 'old_list' to 'old_list' using fr_pair_list_move()"); - fr_pair_list_move(&new_list, &old_list); + TEST_CASE("Move pair from 'old_list' to 'new_list' using fr_pair_list_move()"); + fr_pair_list_move(&new_list, &old_list, T_OP_ADD); TEST_CASE("Looking for Test-Uint32-0"); TEST_CHECK((vp = fr_pair_find_by_da(&new_list, fr_dict_attr_test_uint32, 0)) != NULL); diff --git a/src/lib/util/token.c b/src/lib/util/token.c index e870d37a0b6..aea660375f0 100644 --- a/src/lib/util/token.c +++ b/src/lib/util/token.c @@ -47,6 +47,7 @@ fr_table_num_ordered_t const fr_tokens_table[] = { { L("=*"), T_OP_CMP_TRUE }, { L("!*"), T_OP_CMP_FALSE }, { L("=="), T_OP_CMP_EQ }, + { L("^="), T_OP_PREPEND }, { L("="), T_OP_EQ }, { L("!="), T_OP_NE }, { L(">="), T_OP_GE }, @@ -94,6 +95,7 @@ char const *fr_tokens[] = { "=*", "!*", "==", + "^=", "#", "", "<\"STRING\">", @@ -132,9 +134,10 @@ const char fr_token_quote[] = { '?', /* =* 20 */ '?', /* !* */ '?', /* == */ - '?', /* # */ - '\0', /* bare word */ - '"', /* "foo" 25 */ + '?', /* ^= */ + '?', /* # */ + '\0', /* bare word 25 */ + '"', /* "foo" */ '\'', /* 'foo' */ '`', /* `foo` */ '/', /* /foo/ */ @@ -166,9 +169,10 @@ const bool fr_assignment_op[] = { false, /* =* 20 */ false, /* !* */ false, /* == */ - false, /* # */ - false, /* bare word */ - false, /* "foo" 25 */ + true, /* ^= */ + false, /* # */ + false, /* bare word 25 */ + false, /* "foo" */ false, /* 'foo' */ false, /* `foo` */ false @@ -199,9 +203,10 @@ const bool fr_equality_op[] = { true, /* =* 20 */ true, /* !* */ true, /* == */ - false, /* # */ - false, /* bare word */ - false, /* "foo" 25 */ + false, /* ^= */ + false, /* # */ + false, /* bare word 25 */ + false, /* "foo" */ false, /* 'foo' */ false, /* `foo` */ false @@ -232,9 +237,10 @@ const bool fr_str_tok[] = { false, /* =* 20 */ false, /* !* */ false, /* == */ - false, /* # */ - true, /* bare word */ - true, /* "foo" 25 */ + false, /* ^= */ + false, /* # */ + true, /* bare word 25 */ + true, /* "foo" */ true, /* 'foo' */ true, /* `foo` */ false diff --git a/src/lib/util/token.h b/src/lib/util/token.h index e149fb1c706..d69449e7ef0 100644 --- a/src/lib/util/token.h +++ b/src/lib/util/token.h @@ -60,9 +60,10 @@ typedef enum fr_token { T_OP_CMP_TRUE, /* =* 20 */ T_OP_CMP_FALSE, /* !* */ T_OP_CMP_EQ, /* == */ + T_OP_PREPEND, /* ^= */ T_HASH, /* # */ - T_BARE_WORD, /* bare word */ - T_DOUBLE_QUOTED_STRING, /* "foo" 25 */ + T_BARE_WORD, /* bare word 25 */ + T_DOUBLE_QUOTED_STRING, /* "foo" */ T_SINGLE_QUOTED_STRING, /* 'foo' */ T_BACK_QUOTED_STRING, /* `foo` */ T_SOLIDUS_QUOTED_STRING, /* /foo/ */ @@ -70,7 +71,7 @@ typedef enum fr_token { } fr_token_t; #define T_EQSTART T_OP_ADD -#define T_EQEND (T_OP_CMP_EQ + 1) +#define T_EQEND (T_OP_PREPEND + 1) /** Macro to use as dflt * diff --git a/src/modules/rlm_csv/rlm_csv.c b/src/modules/rlm_csv/rlm_csv.c index 92f1e7b53b9..f62b9f56279 100644 --- a/src/modules/rlm_csv/rlm_csv.c +++ b/src/modules/rlm_csv/rlm_csv.c @@ -459,6 +459,7 @@ static int csv_map_verify(map_t *map, void *instance) case T_OP_EQ: case T_OP_SUB: case T_OP_ADD: + case T_OP_PREPEND: case T_OP_LT: case T_OP_GT: case T_OP_LE: diff --git a/src/modules/rlm_dhcpv4/rlm_dhcpv4.c b/src/modules/rlm_dhcpv4/rlm_dhcpv4.c index 7a03b683756..4f97e26267d 100644 --- a/src/modules/rlm_dhcpv4/rlm_dhcpv4.c +++ b/src/modules/rlm_dhcpv4/rlm_dhcpv4.c @@ -118,7 +118,7 @@ static xlat_action_t dhcpv4_decode_xlat(TALLOC_CTX *ctx, fr_dcursor_t *out, decoded++; } - fr_pair_list_move(&request->request_pairs, &head); + fr_pair_list_move(&request->request_pairs, &head, T_OP_ADD); /* Free any unmoved pairs */ fr_pair_list_free(&head); diff --git a/src/modules/rlm_exec/rlm_exec.c b/src/modules/rlm_exec/rlm_exec.c index c81e3a51e5f..e7e279f7f60 100644 --- a/src/modules/rlm_exec/rlm_exec.c +++ b/src/modules/rlm_exec/rlm_exec.c @@ -370,7 +370,7 @@ static unlang_action_t mod_exec_wait_resume(rlm_rcode_t *p_result, module_ctx_t ctx = tmpl_list_ctx(request, inst->output_list); fr_pair_list_afrom_box(ctx, &vps, request->dict, box); - if (!fr_pair_list_empty(&vps)) fr_pair_list_move(output_pairs, &vps); + if (!fr_pair_list_empty(&vps)) fr_pair_list_move(output_pairs, &vps, T_OP_ADD); fr_dlist_talloc_free(&m->box); /* has been consumed */ } diff --git a/src/modules/rlm_files/rlm_files.c b/src/modules/rlm_files/rlm_files.c index 997e97d275a..92b8a484c0e 100644 --- a/src/modules/rlm_files/rlm_files.c +++ b/src/modules/rlm_files/rlm_files.c @@ -531,7 +531,7 @@ redo: /* * Move the control items over, too. */ - fr_pair_list_move(&request->control_pairs, &list); + fr_pair_list_move(&request->control_pairs, &list, T_OP_ADD); fr_pair_list_free(&list); /* ctx may be reply */ diff --git a/src/tests/keywords/map-csv-prepend b/src/tests/keywords/map-csv-prepend new file mode 100644 index 00000000000..e3e3bd5f54c --- /dev/null +++ b/src/tests/keywords/map-csv-prepend @@ -0,0 +1,25 @@ +# +# PRE: update map +# + +update control { + &Tmp-String-0 := "fail" +} + +map csv &User-Name { + &control.Tmp-String-0 ^= 'field3' +} + +if ("%{control.Tmp-String-0[0]}" != 'success') { + test_fail +} + +if ("%{control.Tmp-String-0[1]}" != "fail") { + test_fail +} + +if ("%{control.Tmp-String-0[#]}" != 2) { + test_fail +} + +success diff --git a/src/tests/keywords/update-prepend b/src/tests/keywords/update-prepend new file mode 100644 index 00000000000..086511943e1 --- /dev/null +++ b/src/tests/keywords/update-prepend @@ -0,0 +1,83 @@ +# +# PRE: update +# + +# Define initial test strings +update control { + &Tmp-String-0 := 'foo' + &Tmp-String-0 += 'baz' +} + +# Reset the request list and add the test strings +# FIXME: +# Temporary way to add multiple copies of an attribute - this should become +# &request.Tmp-String-0 += &control.Tmp-String-0 when multi copies of attributes +# are copied again. Currently the desired syntax won't work due to tmpl_t only holding +# a single value_box - so when mapt_to_list_mod() builds the list of value for the rhs +# of the expression only the first one is copied to the tmpl_t. See around line 780 +# of map_async.c +update { + &request.Tmp-String-0 !* ANY + &request.Tmp-String-0 += &control.Tmp-String-0[0] + &request.Tmp-String-0 += &control.Tmp-String-0[1] +} + +# Prepend a single value +update request { + &Tmp-String-0 ^= 'boink' +} + +# The prepended value should be first followd by the other two +if (("%{Tmp-String-0[0]}" != 'boink') || ("%{Tmp-String-0[1]}" != 'foo') || ("%{Tmp-String-0[2]}" != 'baz')) { + test_fail +} + +if ("%{Tmp-String-0[#]}" != 3) { + test_fail +} + +# Add an extra element to the start of control +update control { + &Tmp-String-0 ^= 'wibble' +} + +# Prepend control to request +update { + &request ^= &control +} + +# The attributes should now be "wibble", "foo", "baz", "boink", "foo", "baz" +if (("%{Tmp-String-0[0]}" != 'wibble') || ("%{Tmp-String-0[1]}" != 'foo') || ("%{Tmp-String-0[2]}" != 'baz') || ("%{Tmp-String-0[3]}" != 'boink') || ("%{Tmp-String-0[4]}" != 'foo') || ("%{Tmp-String-0[5]}" != 'baz')) { + test_fail +} + +if ("%{Tmp-String-0[#]}" != 6) { + test_fail +} + +# Set up an attribute in control with a single value +update control { + &Tmp-String-0 := 'initial' +} + +# Prepend the list of Tmp-String-0 from request to the new attribute +#update { +# &control.Tmp-String-0 ^= &request.Tmp-String-0 +#} + +# (Temporary method to acheive the same - as per issue noted above) +update { + &control ^= &request +} + +# The control attributes should now be "wibble", "foo", "baz", "boink", "foo", "baz", "initial" +if (("%{control.Tmp-String-0[0]}" != 'wibble') || ("%{control.Tmp-String-0[1]}" != 'foo') || ("%{control.Tmp-String-0[2]}" != 'baz') || ("%{control.Tmp-String-0[3]}" != 'boink') || ("%{control.Tmp-String-0[4]}" != 'foo') || ("%{control.Tmp-String-0[5]}" != 'baz') || ("%{control.Tmp-String-0[6]}" != 'initial')) { + test_fail +} + +if ("%{control.Tmp-String-0[#]}" != 7) { + test_fail +} + + +success diff --git a/src/tests/modules/json/eval.unlang b/src/tests/modules/json/eval.unlang index 0ef43f91dce..bcf13ff6c61 100644 --- a/src/tests/modules/json/eval.unlang +++ b/src/tests/modules/json/eval.unlang @@ -105,6 +105,28 @@ update request { &Tmp-Integer-0 !* ANY } +# 9a. All of the array +map json &Tmp-String-0 { + &Tmp-Integer-0 += '$.my_array.*' +} +if ((&Tmp-Integer-0[0] != 0) || (&Tmp-Integer-0[1] != 1) || (&Tmp-Integer-0[2] != 2) || (&Tmp-Integer-0[3] != 3) || (&Tmp-Integer-0[4] != 4) || (&Tmp-Integer-0[5] != 5)) { + test_fail +} + +update request { + &Tmp-Integer-0 := 9 +} +# 9b. All of the array using prepend, places the array before the existing value +map json &Tmp-String-0 { + &Tmp-Integer-0 ^= '$.my_array.*' +} +if ((&Tmp-Integer-0[0] != 0) || (&Tmp-Integer-0[1] != 1) || (&Tmp-Integer-0[2] != 2) || (&Tmp-Integer-0[3] != 3) || (&Tmp-Integer-0[4] != 4) || (&Tmp-Integer-0[5] != 5) || (&Tmp-Integer-0[6] != 9)) { + test_fail +} +update request { + &Tmp-Integer-0 !* ANY +} + # 10. End of the array map json &Tmp-String-0 { &Tmp-Integer-0 := '$.my_array[5]' # Past the end of the array (should be skipped) diff --git a/src/tests/modules/rest/rest_module.unlang b/src/tests/modules/rest/rest_module.unlang index 53282afb2f4..fd01b9923bb 100644 --- a/src/tests/modules/rest/rest_module.unlang +++ b/src/tests/modules/rest/rest_module.unlang @@ -1,3 +1,8 @@ +# Pre-set Tmp-String-2 to check correct operator behaviour +update control { + &Tmp-String-2 := "foo" +} + # Test "authorize" rest call. Uses http to a GET end point rest @@ -23,6 +28,11 @@ if (&control.User-Name != "Bob") { test_fail } +# The "op" for setting Tmp-String-2 is ^= +if ((&control.Tmp-String-2[0] != "Bob") || (&control.Tmp-String-2[1] != "foo")) { + test_fail +} + # Reset control attributes update control { &Tmp-String-0 !* ANY @@ -30,6 +40,11 @@ update control { &User-Name !* ANY } +# Pre-fill NAS-IP-Address to check operator behaviour +update control { + &NAS-IP-Address := "10.0.0.10" +} + # Test "accounting" rest call. Uses https to a POST end point rest.accounting @@ -53,8 +68,12 @@ if (&control.User-Name != "Bob") { test_fail } +if ((&control.Tmp-String-2[0] != "Bob") || (&control.Tmp-String-2[1] != "Bob") || (&control.Tmp-String-2[2] != "foo")) { + test_fail +} + # NAS IP Address is passed in body data -if (&control.NAS-IP-Address != "192.168.1.1") { +if ((&control.NAS-IP-Address[0] != "10.0.0.10") || (&control.NAS-IP-Address[1] != "192.168.1.1")) { test_fail } diff --git a/src/tests/modules/rest/rest_xlat.unlang b/src/tests/modules/rest/rest_xlat.unlang index 65a3f960310..30a6ff95b84 100644 --- a/src/tests/modules/rest/rest_xlat.unlang +++ b/src/tests/modules/rest/rest_xlat.unlang @@ -53,6 +53,19 @@ if (&control.User-Name != "Bob") { test_fail } +update control { + &Tmp-String-3 := 'dummy' +} + +# Directly use json map and prepend the returned value +map json "%{rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/mac/%{Called-Station-Id}}" { + &control.Tmp-String-3 ^= '$.control\.User-Name.value' +} + +if ((&control.Tmp-String-3[0] != 'Bob') || (&control.Tmp-String-3[1] != 'dummy')) { + test_fail +} + update control { &Tmp-String-2 = "%{json_encode:&request.NAS-IP-Address}" } diff --git a/src/tests/modules/sql/map.unlang b/src/tests/modules/sql/map.unlang index e112cba4890..69831909a8f 100644 --- a/src/tests/modules/sql/map.unlang +++ b/src/tests/modules/sql/map.unlang @@ -167,6 +167,38 @@ update { &control.Tmp-Integer-0 !* ANY } +# Retrieve our test row(s) - With ^= we should get the values from the second row then the first +map sql 'SELECT * FROM radusergroup WHERE priority = 0' { + &control.Tmp-String-0 ^= 'username' + &control.Tmp-String-1 ^= 'groupname' + &control.Tmp-Integer-0 ^= 'priority' +} + +if (!updated) { + test_fail +} + +debug_control + +if ((&control.Tmp-String-0[0] != 'oof') || (&control.Tmp-String-0[1] != 'bob')) { + test_fail +} + +if ((&control.Tmp-String-1[0] != 'rab') || (&control.Tmp-String-1[1] != 'bar')) { + test_fail +} + +if ((&control.Tmp-Integer-0[0] != 0) || (&control.Tmp-Integer-0[1] != 0)) { + test_fail +} + +# Clear the control list +update { + &control.Tmp-String-0 !* ANY + &control.Tmp-String-1 !* ANY + &control.Tmp-Integer-0 !* ANY +} + # Retrieve our test row(s) - With += we should get the values from both rows map sql 'SELECT * FROM radusergroup WHERE priority = 0' { &control.Tmp-String-0 += 'username' @@ -364,3 +396,4 @@ else { test_fail } +test_pass \ No newline at end of file