From: Alan T. DeKok Date: Wed, 13 Dec 2023 21:54:58 +0000 (-0500) Subject: add the "transaction" keyword. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e78dc97834e157bde1307108b375d42542303ca2;p=thirdparty%2Ffreeradius-server.git add the "transaction" keyword. For now, the compile hack "all_edits" remains. It should be removed once the tests have been updated to use transactions --- diff --git a/doc/antora/modules/reference/pages/unlang/actions.adoc b/doc/antora/modules/reference/pages/unlang/actions.adoc index a91abc96f7a..553eadea4cd 100644 --- a/doc/antora/modules/reference/pages/unlang/actions.adoc +++ b/doc/antora/modules/reference/pages/unlang/actions.adoc @@ -30,6 +30,7 @@ following keywords: * `elsif` * `group` * `timeout` +* `transaction` == Priorities diff --git a/doc/antora/modules/reference/pages/unlang/keywords.adoc b/doc/antora/modules/reference/pages/unlang/keywords.adoc index c6eae274794..239805b42d5 100644 --- a/doc/antora/modules/reference/pages/unlang/keywords.adoc +++ b/doc/antora/modules/reference/pages/unlang/keywords.adoc @@ -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 index 00000000000..e2b34fd7987 --- /dev/null +++ b/doc/antora/modules/reference/pages/unlang/transaction.adoc @@ -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. diff --git a/src/lib/unlang/all.mk b/src/lib/unlang/all.mk index 3ae74b7faba..5d8c86623ce 100644 --- a/src/lib/unlang/all.mk +++ b/src/lib/unlang/all.mk @@ -25,6 +25,7 @@ SOURCES := base.c \ switch.c \ timeout.c \ tmpl.c \ + transaction.c \ xlat.c \ xlat_alloc.c \ xlat_builtin.c \ diff --git a/src/lib/unlang/base.c b/src/lib/unlang/base.c index b03d50a8352..11d25d90b6c 100644 --- a/src/lib/unlang/base.c +++ b/src/lib/unlang/base.c @@ -122,6 +122,7 @@ int unlang_init_global(void) unlang_edit_init(); unlang_timeout_init(); unlang_limit_init(); + unlang_transaction_init(); instance_count++; diff --git a/src/lib/unlang/compile.c b/src/lib/unlang/compile.c index e727bdf168c..4014a7338fd 100644 --- a/src/lib/unlang/compile.c +++ b/src/lib/unlang/compile.c @@ -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); diff --git a/src/lib/unlang/edit.c b/src/lib/unlang/edit.c index cf456cbdf02..a296cffd0c9 100644 --- a/src/lib/unlang/edit.c +++ b/src/lib/unlang/edit.c @@ -32,6 +32,7 @@ RCSID("$Id$") #include #include #include +#include #include #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. diff --git a/src/lib/unlang/interpret.c b/src/lib/unlang/interpret.c index 4ceae8df2ae..2bbd7427bc6 100644 --- a/src/lib/unlang/interpret.c +++ b/src/lib/unlang/interpret.c @@ -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 index 00000000000..4e8118c7bb7 --- /dev/null +++ b/src/lib/unlang/transaction.c @@ -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 +#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 index 00000000000..42deac0f5aa --- /dev/null +++ b/src/lib/unlang/transaction.h @@ -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 +#include + +#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 index 00000000000..315ca349969 --- /dev/null +++ b/src/lib/unlang/transaction_priv.h @@ -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 + +#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 diff --git a/src/lib/unlang/unlang_priv.h b/src/lib/unlang/unlang_priv.h index e275891ea39..9c4b7f5bf25 100644 --- a/src/lib/unlang/unlang_priv.h +++ b/src/lib/unlang/unlang_priv.h @@ -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 index 00000000000..f26ec8c3be0 --- /dev/null +++ b/src/tests/keywords/transaction @@ -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