| 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.
|===
* 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.
====
| 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.
| 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
| `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]
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.
|=====
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
.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"
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
)
op = ":=",
value = keyData.username
}
+ returnData["control.Tmp-String-2"] = {
+ op = "^=",
+ value = keyData.username
+ }
return ngx.say(cjson.encode(returnData))
end
)
* 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) {
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) {
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);
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;
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);
* 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;
case T_OP_EQ:
case T_OP_SET:
+ case T_OP_PREPEND:
fr_skip_whitespace(ptr);
op_token = name2_token;
break;
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;
}
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
*/
}
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;
}
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
*/
{
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;
*/
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
*/
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++) {
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.
}
/*
- * Only += and :=, and !* operators are supported
+ * Only += and :=, and !*, and ^= operators are supported
* for lists.
*/
switch (map->op) {
}
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>"));
# 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)
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.
--- /dev/null
+/*
+ * 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);
+}
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()
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);
*
* @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;
*/
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.
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);
+
}
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
}
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);
{ 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 },
"=*",
"!*",
"==",
+ "^=",
"#",
"<BARE-WORD>",
"<\"STRING\">",
'?', /* =* 20 */
'?', /* !* */
'?', /* == */
- '?', /* # */
- '\0', /* bare word */
- '"', /* "foo" 25 */
+ '?', /* ^= */
+ '?', /* # */
+ '\0', /* bare word 25 */
+ '"', /* "foo" */
'\'', /* 'foo' */
'`', /* `foo` */
'/', /* /foo/ */
false, /* =* 20 */
false, /* !* */
false, /* == */
- false, /* # */
- false, /* bare word */
- false, /* "foo" 25 */
+ true, /* ^= */
+ false, /* # */
+ false, /* bare word 25 */
+ false, /* "foo" */
false, /* 'foo' */
false, /* `foo` */
false
true, /* =* 20 */
true, /* !* */
true, /* == */
- false, /* # */
- false, /* bare word */
- false, /* "foo" 25 */
+ false, /* ^= */
+ false, /* # */
+ false, /* bare word 25 */
+ false, /* "foo" */
false, /* 'foo' */
false, /* `foo` */
false
false, /* =* 20 */
false, /* !* */
false, /* == */
- false, /* # */
- true, /* bare word */
- true, /* "foo" 25 */
+ false, /* ^= */
+ false, /* # */
+ true, /* bare word 25 */
+ true, /* "foo" */
true, /* 'foo' */
true, /* `foo` */
false
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/ */
} 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
*
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:
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);
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 */
}
/*
* 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 */
--- /dev/null
+#
+# 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
--- /dev/null
+#
+# 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
&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)
+# 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
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
&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
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
}
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}"
}
&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'
test_fail
}
+test_pass
\ No newline at end of file