* `elsif`
* `group`
* `timeout`
+* `transaction`
== Priorities
| xref:unlang/if.adoc[if] | Check for a condition, and execute a sub-policy if it matches.
| xref:unlang/return.adoc[return] | Immediately stop processing a section.
| xref:unlang/switch.adoc[switch] | Check for multiple values.
+| xref:unlang/transaction.adoc[transaction] | Group operations into a transaction which can be reverted on failure.
|=====
== Attribute Editing Keywords
--- /dev/null
+= The transaction Statement
+
+.Syntax
+[source,unlang]
+----
+transaction {
+ [ statements ]
+}
+----
+
+The `transaction` statement collects a series of statements into a
+single transaction. If the block returns an error such as `fail`,
+`reject`, `invalid`, or `disallow`, any attribute changes which have
+been made will be reverted.
+
+[ statements ]:: The `unlang` commands which will be executed.
+
+== Caveats
+
+The `transaction` keyword currently has some major limitations.
+
+The first limitation is that most processing sections have a default
+action of `return` for failures. This setting means that failures
+cause the server to stop processing the entire section. The
+`transaction` keyword is no different.
+
+If instead the server should continue processing the next statement
+after the `transaction`, then the `transaction` section must finish
+with an xref:unlang/actions.adoc[actions] subsection, as given in the
+example below. This subsection over-ride sthe default action of
+`return` for failures, and allows processing to continue after the
+`transaction`.
+
+The second limitation of `transaction` is that it can only revert
+attribute editing which is done via the xref:unlang/edit.adoc[edit]
+statements. If a module performs attribute editing (e.g. `sql`,
+`files`, etc.), then those edits are not reverted.
+
+Similarly, a `transaction` cannot undo operations on external
+databases. For example, any data which was inserted into `sql` during
+a `transaction` statement will remain in `sql`.
+
+.Examples
+
+[source,unlang]
+----
+transaction {
+ &reply.Filter-Id := %exec
+ sql
+
+ actions {
+ fail = 1
+ }
+}
+----
+
+The last entry in a `transaction` section can also be an xref:unlang/actions.adoc[actions] subsection.
+
+== Grouping Edits
+
+The `transaction` keyword can be used to group multiple
+xref:unlang/edit.adoc[edit] instructions. When edit instructions are
+grouped, then the edits are done in an atomic transaction. That is,
+either all of the edits succeed, or none of them do.
+
+For now, the only purpose of `transaction` is to group edits.
+
+// Copyright (C) 2023 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// This documentation was developed by Network RADIUS SAS.
switch.c \
timeout.c \
tmpl.c \
+ transaction.c \
xlat.c \
xlat_alloc.c \
xlat_builtin.c \
unlang_edit_init();
unlang_timeout_init();
unlang_limit_init();
+ unlang_transaction_init();
instance_count++;
#include "edit_priv.h"
#include "timeout_priv.h"
#include "limit_priv.h"
+#include "transaction_priv.h"
#define UNLANG_IGNORE ((unlang_t *) -1)
case UNLANG_TYPE_SWITCH:
case UNLANG_TYPE_TIMEOUT:
case UNLANG_TYPE_LIMIT:
+ case UNLANG_TYPE_TRANSACTION:
g = unlang_generic_to_group(c);
DEBUG("%.*s%s {", depth, unlang_spaces, c->debug_name);
unlang_dump(g->children, depth + 1);
case UNLANG_TYPE_ELSIF:
case UNLANG_TYPE_GROUP:
case UNLANG_TYPE_TIMEOUT:
+ case UNLANG_TYPE_TRANSACTION:
break;
default:
fr_assert(unlang_ctx->rules != NULL);
fr_assert(unlang_ctx->rules->attr.list_def);
+
/*
* We always create a group, even if the section is empty.
*/
return compile_section(parent, unlang_ctx, cs, &group);
}
+static fr_table_num_sorted_t transaction_keywords[] = {
+ { L("case"), 1 },
+ { L("else"), 1 },
+ { L("elsif"), 1 },
+ { L("foreach"), 1 },
+ { L("group"), 1 },
+ { L("if"), 1 },
+ { L("limit"), 1 },
+ { L("load-balance"), 1 },
+ { L("redundant"), 1 },
+ { L("redundant-load-balance"), 1 },
+ { L("switch"), 1 },
+ { L("timeout"), 1 },
+ { L("transaction"), 1 },
+};
+static int transaction_keywords_len = NUM_ELEMENTS(transaction_keywords);
+
+/** Limit the operations which can appear in a transaction.
+ *
+ * We probably want this to be
+ */
+static bool transaction_ok(CONF_SECTION *cs, bool *all_edits)
+{
+ CONF_ITEM *ci = NULL;
+
+ while ((ci = cf_item_next(cs, ci)) != NULL) {
+ char const *name;
+
+ if (cf_item_is_section(ci)) {
+ CONF_SECTION *subcs;
+
+ subcs = cf_item_to_section(ci);
+ name = cf_section_name1(subcs);
+
+ if (strcmp(name, "actions") == 0) continue;
+
+ /*
+ * Definitely an attribute editing thing.
+ */
+ if (*name == '&') continue;
+
+ if (fr_table_value_by_str(transaction_keywords, name, -1) < 0) goto fail;
+
+ *all_edits = false;
+
+ if (!transaction_ok(subcs, all_edits)) return false;
+
+ continue;
+
+ } else if (cf_item_is_pair(ci)) {
+ CONF_PAIR *cp;
+
+ cp = cf_item_to_pair(ci);
+ name = cf_pair_attr(cp);
+
+ if (*name == '&') continue;
+
+ /*
+ * Allow rcodes via the "always" module.
+ */
+ if (fr_table_value_by_str(mod_rcode_table, name, -1) >= 0) {
+ *all_edits = false;
+ continue;
+ }
+
+ /*
+ * For now, don't support expansions on the LHS.
+ *
+ * And don't support in-line functions.
+ */
+ fail:
+ cf_log_err(ci, "Unexpected contents in 'transaction'");
+ return false;
+ } else {
+ continue;
+ }
+ }
+
+ return true;
+}
+
+static unlang_t *compile_transaction(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF_SECTION *cs)
+{
+ unlang_t *c;
+ bool all_edits = true;
+ unlang_compile_t unlang_ctx2;
+
+ static unlang_ext_t const transaction = {
+ .type = UNLANG_TYPE_TRANSACTION,
+ .len = sizeof(unlang_transaction_t),
+ .type_name = "unlang_transaction_t",
+ };
+
+ if (!transaction_ok(cs, &all_edits)) return NULL;
+
+ /*
+ * Any failure is return, not continue.
+ */
+ compile_copy_context(&unlang_ctx2, unlang_ctx, unlang_ctx->component);
+
+ unlang_ctx2.actions.actions[RLM_MODULE_REJECT] = MOD_ACTION_RETURN;
+ unlang_ctx2.actions.actions[RLM_MODULE_FAIL] = MOD_ACTION_RETURN;
+ unlang_ctx2.actions.actions[RLM_MODULE_INVALID] = MOD_ACTION_RETURN;
+ unlang_ctx2.actions.actions[RLM_MODULE_DISALLOW] = MOD_ACTION_RETURN;
+
+ unlang_ctx2.all_edits = all_edits;
+
+ c = compile_section(parent, &unlang_ctx2, cs, &transaction);
+ if (!c || (c == UNLANG_IGNORE)) return c;
+
+ c->type = UNLANG_TYPE_TRANSACTION;
+ return c;
+}
+
+
static int8_t case_cmp(void const *one, void const *two)
{
unlang_case_t const *a = (unlang_case_t const *) one; /* may not be talloc'd! See switch.c */
{ L("subrequest"), (void *) compile_subrequest },
{ L("switch"), (void *) compile_switch },
{ L("timeout"), (void *) compile_timeout },
+ { L("transaction"), (void *) compile_transaction },
{ L("update"), (void *) compile_update },
};
static int unlang_section_keywords_len = NUM_ELEMENTS(unlang_section_keywords);
#include <freeradius-devel/util/calc.h>
#include <freeradius-devel/unlang/tmpl.h>
#include <freeradius-devel/unlang/edit.h>
+#include <freeradius-devel/unlang/transaction.h>
#include <freeradius-devel/unlang/unlang_priv.h>
#include "edit_priv.h"
struct unlang_frame_state_edit_s {
fr_edit_list_t *el; //!< edit list
bool *success; //!< whether or not the edit succeeded
+ bool ours;
rindent_t indent;
*/
if (state->success) *state->success = false;
- fr_edit_list_abort(state->el);
+ if (state->ours) fr_edit_list_abort(state->el);
TALLOC_FREE(frame->state);
repeatable_clear(frame);
*p_result = RLM_MODULE_NOOP;
*/
if (!el) {
MEM(state->el = fr_edit_list_alloc(state, map_list_num_elements(map_list), NULL));
+ state->ours = true;
+ } else {
+ state->el = el;
+ state->ours = false;
}
current->ctx = state;
{
unlang_edit_t *edit = unlang_generic_to_edit(frame->instruction);
unlang_frame_state_edit_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_edit_t);
+ fr_edit_list_t *el = unlang_interpret_edit_list(request);
- edit_state_init_internal(request, state, NULL, &edit->maps);
+ edit_state_init_internal(request, state, el, &edit->maps);
/*
* Call process_edit to do all of the work.
DUMP_STACK;
fr_assert(instruction->debug_name != NULL); /* if this happens, all bets are off. */
+ fr_assert(unlang_ops[instruction->type].interpret != NULL);
REQUEST_VERIFY(request);
--- /dev/null
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file unlang/transaction.c
+ * @brief Allows for edit transactions
+ *
+ * @copyright 2023 Network RADIUS SAS (legal@networkradius.com)
+ */
+
+RCSID("$Id$")
+
+#include <freeradius-devel/util/syserror.h>
+#include "transaction.h"
+#include "transaction_priv.h"
+
+/** Signal a transaction to abort.
+ *
+ * @param[in] request The current request.
+ * @param[in] frame being signalled.
+ * @param[in] action to signal.
+ */
+static void unlang_transaction_signal(UNUSED request_t *request, unlang_stack_frame_t *frame, fr_signal_t action)
+{
+ unlang_frame_state_transaction_t *state = talloc_get_type_abort(frame->state,
+ unlang_frame_state_transaction_t);
+
+ /*
+ * Ignore everything except cancel.
+ */
+ if (action != FR_SIGNAL_CANCEL) return;
+
+ fr_edit_list_abort(state->el);
+ state->el = NULL;
+}
+
+/** Commit a successful transaction.
+ *
+ */
+static unlang_action_t unlang_transaction_final(rlm_rcode_t *p_result, UNUSED request_t *request,
+ unlang_stack_frame_t *frame)
+{
+ unlang_frame_state_transaction_t *state = talloc_get_type_abort(frame->state,
+ unlang_frame_state_transaction_t);
+
+ fr_assert(state->el != NULL);
+
+ switch (*p_result) {
+ case RLM_MODULE_REJECT:
+ case RLM_MODULE_FAIL:
+ case RLM_MODULE_INVALID:
+ case RLM_MODULE_DISALLOW:
+ fr_edit_list_abort(state->el);
+ break;
+
+ case RLM_MODULE_OK:
+ case RLM_MODULE_HANDLED:
+ case RLM_MODULE_NOOP:
+ case RLM_MODULE_UPDATED:
+ fr_edit_list_commit(state->el);
+ break;
+
+ default:
+ fr_assert(0);
+ return UNLANG_ACTION_FAIL;
+ }
+
+ return UNLANG_ACTION_CALCULATE_RESULT;
+}
+
+static unlang_action_t unlang_transaction(rlm_rcode_t *p_result, request_t *request, unlang_stack_frame_t *frame)
+{
+ unlang_frame_state_transaction_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_transaction_t);
+ fr_edit_list_t *parent;
+
+ parent = unlang_interpret_edit_list(request);
+
+ MEM(state->el = fr_edit_list_alloc(state, 10, parent));
+
+ frame_repeat(frame, unlang_transaction_final);
+
+ return unlang_interpret_push_children(p_result, request, frame->result, UNLANG_NEXT_SIBLING);
+}
+
+fr_edit_list_t *unlang_interpret_edit_list(request_t *request)
+{
+ unlang_stack_frame_t *frame;
+ unlang_stack_t *stack = request->stack;
+ int i, depth = stack->depth;
+
+ if (depth == 1) return NULL;
+
+ for (i = depth - 1; i > 0; i--) {
+ unlang_frame_state_transaction_t *state;
+
+ frame = &stack->frame[i];
+ if (frame->instruction->type != UNLANG_TYPE_TRANSACTION) continue;
+
+ state = talloc_get_type_abort(frame->state, unlang_frame_state_transaction_t);
+ fr_assert(state->el != NULL);
+
+ return state->el;
+ }
+
+ return NULL;
+}
+
+void unlang_transaction_init(void)
+{
+ unlang_register(UNLANG_TYPE_TRANSACTION,
+ &(unlang_op_t){
+ .name = "transaction",
+ .interpret = unlang_transaction,
+ .signal = unlang_transaction_signal,
+ .frame_state_size = sizeof(unlang_frame_state_transaction_t),
+ .frame_state_type = "unlang_frame_state_transaction_t",
+ .debug_braces = true
+ });
+}
--- /dev/null
+#pragma once
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * $Id$
+ *
+ * @file unlang/transaction.h
+ * @brief Declarations for unlang transactions
+ *
+ * @copyright 2023 Network RADIUS SAS (legal@networkradius.com)
+ */
+#include <freeradius-devel/server/request.h>
+#include <freeradius-devel/util/edit.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+fr_edit_list_t *unlang_interpret_edit_list(request_t *request);
+
+#ifdef __cplusplus
+}
+#endif
--- /dev/null
+#pragma once
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * $Id$
+ *
+ * @file unlang/transaction_priv.h
+ * @brief Declarations for unlang transactions
+ *
+ * @copyright 2023 Network RADIUS SAS (legal@networkradius.com)
+ */
+#include "unlang_priv.h"
+#include <freeradius-devel/util/edit.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+ unlang_group_t group;
+} unlang_transaction_t;
+
+/** A transaction stack entry
+ */
+typedef struct {
+ fr_edit_list_t *el; //!< my edit list
+} unlang_frame_state_transaction_t;
+
+/** Cast a group structure to the transaction keyword extension
+ *
+ */
+static inline unlang_transaction_t *unlang_group_to_transaction(unlang_group_t *g)
+{
+ return talloc_get_type_abort(g, unlang_transaction_t);
+}
+
+/** Cast a transaction keyword extension to a group structure
+ *
+ */
+static inline unlang_group_t *unlang_transaction_to_group(unlang_transaction_t *to)
+{
+ return (unlang_group_t *)to;
+}
+
+#ifdef __cplusplus
+}
+#endif
UNLANG_TYPE_CALLER, //!< conditionally check parent dictionary type
UNLANG_TYPE_TIMEOUT, //!< time-based timeouts.
UNLANG_TYPE_LIMIT, //!< limit number of requests in a section
+ UNLANG_TYPE_TRANSACTION, //!< transactions for editing lists
UNLANG_TYPE_POLICY, //!< Policy section.
UNLANG_TYPE_XLAT, //!< Represents one level of an xlat expansion.
UNLANG_TYPE_TMPL, //!< asynchronously expand a tmpl_t
void unlang_timeout_init(void);
+void unlang_transaction_init(void);
+
void unlang_limit_init(void);
/** @} */
--- /dev/null
+#
+# PRE: if
+#
+string foo
+string bar
+
+transaction {
+ &foo := "hello"
+
+ fail
+
+ &bar := "nope"
+
+ actions {
+ fail = 1
+ }
+}
+
+#
+# This shouldn't have been applied
+#
+if &bar {
+ test_fail
+}
+
+#
+# This should get rolled back, too!
+#
+if &foo {
+ test_fail
+} else {
+ ok # force auth success for the test framework
+}
+
+success