]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
add the "transaction" keyword.
authorAlan T. DeKok <aland@freeradius.org>
Wed, 13 Dec 2023 21:54:58 +0000 (16:54 -0500)
committerAlan T. DeKok <aland@freeradius.org>
Thu, 14 Dec 2023 00:42:57 +0000 (19:42 -0500)
For now, the compile hack "all_edits" remains.  It should be
removed once the tests have been updated to use transactions

13 files changed:
doc/antora/modules/reference/pages/unlang/actions.adoc
doc/antora/modules/reference/pages/unlang/keywords.adoc
doc/antora/modules/reference/pages/unlang/transaction.adoc [new file with mode: 0644]
src/lib/unlang/all.mk
src/lib/unlang/base.c
src/lib/unlang/compile.c
src/lib/unlang/edit.c
src/lib/unlang/interpret.c
src/lib/unlang/transaction.c [new file with mode: 0644]
src/lib/unlang/transaction.h [new file with mode: 0644]
src/lib/unlang/transaction_priv.h [new file with mode: 0644]
src/lib/unlang/unlang_priv.h
src/tests/keywords/transaction [new file with mode: 0644]

index a91abc96f7a65e1d1c753944936598f0d5780ad0..553eadea4cdd89b67b9ec5c9df6f71326e46a997 100644 (file)
@@ -30,6 +30,7 @@ following keywords:
 * `elsif`
 * `group`
 * `timeout`
+* `transaction`
 
 == Priorities
 
index c6eae2747946a0a8174aeedf156708f5582057b3..239805b42d584a367f7abf856d2c4200640ed324 100644 (file)
@@ -21,6 +21,7 @@ looping, etc.
 | 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
diff --git a/doc/antora/modules/reference/pages/unlang/transaction.adoc b/doc/antora/modules/reference/pages/unlang/transaction.adoc
new file mode 100644 (file)
index 0000000..e2b34fd
--- /dev/null
@@ -0,0 +1,69 @@
+= 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.
index 3ae74b7faba5d628347b4c0c024553f22dee5be3..5d8c86623cefa594be93bb37da48651ff2214b56 100644 (file)
@@ -25,6 +25,7 @@ SOURCES       :=      base.c \
                switch.c \
                timeout.c \
                tmpl.c \
+               transaction.c \
                xlat.c \
                xlat_alloc.c \
                xlat_builtin.c \
index b03d50a835212e64bdc27ad568e0bc4e6322ab02..11d25d90b6ca055143b51e081e022ad90cb47bd3 100644 (file)
@@ -122,6 +122,7 @@ int unlang_init_global(void)
        unlang_edit_init();
        unlang_timeout_init();
        unlang_limit_init();
+       unlang_transaction_init();
 
        instance_count++;
 
index e727bdf168c28b8bfdfaffa0b9b2bb38231627d4..4014a7338fd217379f86a9b0d68473de96b98646 100644 (file)
@@ -44,6 +44,7 @@ RCSID("$Id$")
 #include "edit_priv.h"
 #include "timeout_priv.h"
 #include "limit_priv.h"
+#include "transaction_priv.h"
 
 #define UNLANG_IGNORE ((unlang_t *) -1)
 
@@ -455,6 +456,7 @@ static void unlang_dump(unlang_t *instruction, int depth)
                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);
@@ -2169,6 +2171,7 @@ static bool compile_action_subsection(unlang_t *c, CONF_SECTION *cs, CONF_SECTIO
        case UNLANG_TYPE_ELSIF:
        case UNLANG_TYPE_GROUP:
        case UNLANG_TYPE_TIMEOUT:
+       case UNLANG_TYPE_TRANSACTION:
                break;
 
        default:
@@ -2454,6 +2457,7 @@ static unlang_t *compile_section(unlang_t *parent, unlang_compile_t *unlang_ctx,
 
        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.
         */
@@ -2542,6 +2546,121 @@ static unlang_t *compile_group(unlang_t *parent, unlang_compile_t *unlang_ctx, C
        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 */
@@ -4502,6 +4621,7 @@ static fr_table_ptr_sorted_t unlang_section_keywords[] = {
        { 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);
index cf456cbdf027f055884ac33c2edc6feb5554a45d..a296cffd0c933f8ec933b0789d745d5ee82d931c 100644 (file)
@@ -32,6 +32,7 @@ RCSID("$Id$")
 #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"
 
@@ -84,6 +85,7 @@ struct edit_map_s {
 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;
 
@@ -1541,7 +1543,7 @@ static unlang_action_t process_edit(rlm_rcode_t *p_result, request_t *request, u
                                 */
                                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;
@@ -1588,6 +1590,10 @@ static void edit_state_init_internal(request_t *request, unlang_frame_state_edit
         */
        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;
@@ -1621,8 +1627,9 @@ static unlang_action_t unlang_edit_state_init(rlm_rcode_t *p_result, request_t *
 {
        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.
index 4ceae8df2ae9d06afb30ebec23fde55e41b28706..2bbd7427bc6d3e3adf11bbea4280b17baf08edfc 100644 (file)
@@ -525,6 +525,7 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame
                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);
 
diff --git a/src/lib/unlang/transaction.c b/src/lib/unlang/transaction.c
new file mode 100644 (file)
index 0000000..4e8118c
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ *   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
+                          });
+}
diff --git a/src/lib/unlang/transaction.h b/src/lib/unlang/transaction.h
new file mode 100644 (file)
index 0000000..42deac0
--- /dev/null
@@ -0,0 +1,37 @@
+#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
diff --git a/src/lib/unlang/transaction_priv.h b/src/lib/unlang/transaction_priv.h
new file mode 100644 (file)
index 0000000..315ca34
--- /dev/null
@@ -0,0 +1,61 @@
+#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
index e275891ea39bc2258b7d6929feaa4907f9e56cff..9c4b7f5bf25424933c355802d9e768c8409833e8 100644 (file)
@@ -77,6 +77,7 @@ typedef enum {
        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
@@ -630,6 +631,8 @@ void                unlang_edit_init(void);
 
 void           unlang_timeout_init(void);
 
+void           unlang_transaction_init(void);
+
 void           unlang_limit_init(void);
 
  /** @} */
diff --git a/src/tests/keywords/transaction b/src/tests/keywords/transaction
new file mode 100644 (file)
index 0000000..f26ec8c
--- /dev/null
@@ -0,0 +1,35 @@
+#
+# 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