]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
v4: Add prepend operator for inserting attributes at the beginning of lists (#3673)
authorNick Porter <nick@portercomputing.co.uk>
Thu, 13 May 2021 14:25:09 +0000 (15:25 +0100)
committerGitHub <noreply@github.com>
Thu, 13 May 2021 14:25:09 +0000 (16:25 +0200)
* 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:

<match key>
<attribute> ^= <value>

which will insert <attribute> = <value> 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

33 files changed:
doc/antora/modules/raddb/pages/mods-available/csv.adoc
doc/antora/modules/raddb/pages/mods-available/json.adoc
doc/antora/modules/raddb/pages/mods-available/ldap.adoc
doc/antora/modules/raddb/pages/mods-available/rest.adoc
doc/antora/modules/reference/pages/unlang/update.adoc
man/man5/unlang.5
man/man5/users.5
scripts/ci/openresty/json-api.lua
src/bin/radsniff.c
src/lib/server/cf_file.c
src/lib/server/map.c
src/lib/server/map_async.c
src/lib/server/pairmove.c
src/lib/unlang/compile.c
src/lib/util/dlist.h
src/lib/util/dpair_legacy.c [new file with mode: 0644]
src/lib/util/pair.c
src/lib/util/pair.h
src/lib/util/pair_legacy.c
src/lib/util/pair_legacy.h
src/lib/util/pair_legacy_tests.c
src/lib/util/token.c
src/lib/util/token.h
src/modules/rlm_csv/rlm_csv.c
src/modules/rlm_dhcpv4/rlm_dhcpv4.c
src/modules/rlm_exec/rlm_exec.c
src/modules/rlm_files/rlm_files.c
src/tests/keywords/map-csv-prepend [new file with mode: 0644]
src/tests/keywords/update-prepend [new file with mode: 0644]
src/tests/modules/json/eval.unlang
src/tests/modules/rest/rest_module.unlang
src/tests/modules/rest/rest_xlat.unlang
src/tests/modules/sql/map.unlang

index 3d6612866af0e26c65bede181a994d63a0eb430e..3d0b5b987d24745a0809a1ac60424bb2bea036c3 100644 (file)
@@ -146,7 +146,7 @@ Where:
 | Parameter   | Description
 | <fr attr>   | Is the destination RADIUS attribute
                 with any valid list and request qualifiers.
-| <op>        | Is any assignment attribute (=, :=, +=, -=).
+| <op>        | Is any assignment attribute (=, :=, +=, ^=, -=).
 | <csv field> | Is the name of a field from the CSV file, as taken
                 from the `fields` configuration item.
 |===
index 081ac46c2b8abe261fa41661743492a8b072ed7d..ff2d7c4d5da90cd4604eaba2b3c35a693ef87b23 100644 (file)
@@ -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.
 ====
 
 
index 930e834536e632fa40053d6a2411ca0ab2ae8c9c..9d04aa6e57dff985486936d1c52a845fc96da4e8 100644 (file)
@@ -125,7 +125,7 @@ Where:
 | Parameter  | Description
 | <fr attr>  | Is the attribute you wish to create,
                with any valid list and request qualifiers.
-| <op>       | Is any assignment operator (`=`, `:=`, `+=`, `-=`).
+| <op>       | Is any assignment operator (`=`, `:=`, `+=`, `^=`, `-=`).
 | <value>    | 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
 | <fr attr>   | Is the destination RADIUS attribute
                 with any valid list and request qualifiers.
-| <op>        | Is any assignment attribute (=, :=, +=, -=).
+| <op>        | Is any assignment attribute (=, :=, +=, ^=, -=).
 | <ldap attr> | Is the attribute associated with user or
                 profile objects in the LDAP directory.
                 If the attribute name is wrapped in double quotes
index 396d44582998f5a87baefb64995fd036df671232..69fc6fa34d0ef75800200522729ce0b947cffb24 100644 (file)
@@ -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]
index a1067f1980f312c3fc656a6b283e5ce98a41d074..856b669c6ce04149d692a830c7cd9c1f9a0189f4 100644 (file)
@@ -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 _<value>_.
 | !*       | Delete all occurances of the attribute, no matter what the value.
 |=====
index 45c2bb0f96cf18afd5dcef24c96a99d43d153586..58d22cc9a1435ab2cf2bc4c713f908b4890def5e 100644 (file)
@@ -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
index 73593fe54ae45759b2d970317b3862e88feecabb..8db266566c0dd96aeb386c24ef06374a505f96d9 100644 (file)
@@ -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"
index 5b252992641743041b0786c68d0efef881bd65bb..add2da15a60a12c9e23ef6e855585a847105362e 100644 (file)
@@ -99,9 +99,13 @@ Api.endpoint('POST', '/user/<username>/mac/<client>',
             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/<username>/mac/<client>',
             op = ":=",
             value = keyData.username
         }
+        returnData["control.Tmp-String-2"] = {
+            op = "^=",
+            value = keyData.username
+        }
         return ngx.say(cjson.encode(returnData))
     end
 )
index d62668dfa7d7df9196fb93de5b5357d32cefc2c5..b2f3f69eb6d61fce2be90618336b352609779af9 100644 (file)
@@ -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);
index 7a05229af30aa62b7ee79ed3efca21a0b97682ae..5f343e69ce52038a1291dccd4a792ccb9abc4841 100644 (file)
@@ -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;
index 3530aa5290c18abbee541705c3874b46c994563f..9f45f5fa69b22002a590722d33769b8074850017 100644 (file)
@@ -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
         */
index f4c7c54447c425b26dec0154299903612ec4e3ce..121d2392b594789079b2ad3bf3304d5d3936dcab 100644 (file)
@@ -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
         */
index cd14c7aa14a9129ff8f595fcf036da776f3b9fd6..55bd9915673dfaba8c13495240020302bf9b87a8 100644 (file)
@@ -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.
index e2fefa226cccd19f580091e06facf0840b02b2f0..0e661c17c595c88662804a2bb1301d4deb47a6ba 100644 (file)
@@ -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, "<INVALID>"));
index 08b63f9c09f22d81e3d2a95b86b7f5d787d05879..6dac2c1145090a4f27c9490e465ef2d3c0e848bb 100644 (file)
@@ -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 (file)
index 0000000..c34beb4
--- /dev/null
@@ -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 <freeradius-devel/util/misc.h>
+#include <freeradius-devel/util/pair.h>
+#include <freeradius-devel/util/pair_legacy.h>
+#include <freeradius-devel/util/print.h>
+#include <freeradius-devel/util/proto.h>
+#include <freeradius-devel/util/regex.h>
+#include <freeradius-devel/util/talloc.h>
+
+#include <freeradius-devel/protocol/radius/rfc2865.h>
+#include <freeradius-devel/protocol/freeradius/freeradius.internal.h>
+
+#include <ctype.h>
+
+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);
+}
index b01e84d5e4f3e5282413af10b60496a445a619f9..4fed17cff2d44faa604449122d3ad02cac45dc0f 100644 (file)
@@ -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()
index 5a5cfb87b864930da949a21c0bb37668362816ff..2a668b9389a914d22ef91baa7e5078c7d2a6237b 100644 (file)
@@ -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);
index e9662ebc997cafd5d7da43d42a6bc69c72aba0d5..9b62a14463bb8ded1fda0a73f003fb13b33da163 100644 (file)
@@ -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);
+
 }
index db758a0aab208d09f8d66730e80d14b0a3f330ce..62aa31781f09870c015668886f7471cea7731083 100644 (file)
@@ -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
 }
index 7264f360ecdcf66cb3398ade51a0811f0be12fb3..be40fb29097a899f71c9ab0033019e5653dbcb35 100644 (file)
@@ -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);
index e870d37a0b6a4d181df418679c8fd938692244f3..aea660375f0886dce1d95595c67307c3a7afac20 100644 (file)
@@ -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[] = {
        "=*",
        "!*",
        "==",
+       "^=",
        "#",
        "<BARE-WORD>",
        "<\"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
index e149fb1c706444d2458ed379c75798fcccb39c26..d69449e7ef025933ddbecd514c6c8327b1671c4b 100644 (file)
@@ -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
  *
index 92f1e7b53b905381080d24521ff8fa264b0d3456..f62b9f56279747426d61dc038e7a4e599f367bcb 100644 (file)
@@ -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:
index 7a03b683756f3cb8c94c8c0fcc7476b8ebdd4d4a..4f97e26267d4b991eb6a160aa9ecf9bfef373add 100644 (file)
@@ -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);
index c81e3a51e5fe291188792893de6a1681e83e0c6c..e7e279f7f6010dab5f09aa23e85e349e9042a0d1 100644 (file)
@@ -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 */
        }
index 997e97d275af408619d97e60298d1af64e30ffd8..92b8a484c0ed19ec80c8d4297a9f0babb1f3d355 100644 (file)
@@ -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 (file)
index 0000000..e3e3bd5
--- /dev/null
@@ -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 (file)
index 0000000..0865119
--- /dev/null
@@ -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
index 0ef43f91dce3ad1a98e0873c0d21d4065d1b02a9..bcf13ff6c61be5087ee07d097e63b20fb06a5904 100644 (file)
@@ -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)
index 53282afb2f4a4ded9a0f1af25658dc0f88ed0dfd..fd01b9923bb2d3cb1df06242761fcf9b9ef0b263 100644 (file)
@@ -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
 }
 
index 65a3f960310402967d8a229c676a11eb65576480..30a6ff95b840a3134ba9bacdf37069a067e67302 100644 (file)
@@ -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}"
 }
index e112cba489000d6ba78a5ed0e92e6bb75f887c76..69831909a8f096b5e4120b9d530a74a966aad96d 100644 (file)
@@ -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