From d8971e16fb73868bce873af59d7a912afe0e6e4f Mon Sep 17 00:00:00 2001 From: upstream-mirror Date: Tue, 15 Sep 2026 21:46:19 +0500 Subject: [PATCH] snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted) --- .asf.yaml | 29 + .gitattributes | 21 + .github/scripts/benchmark_formatter.py | 248 ++++ .github/scripts/download_artifact.js | 32 + .github/scripts/post_comment.js | 59 + .github/semantic.yml | 2 + .github/workflows/comment.yml | 37 + .github/workflows/default.yml | 44 + .github/workflows/golangci-lint.yml | 26 + .github/workflows/performance-pr.yml | 97 ++ .github/workflows/release.yml | 204 +++ .gitignore | 30 + .golangci.yml | 198 +++ .rat-excludes | 31 + CONTRIBUTING.md | 35 + DISCLAIMER | 5 + LICENSE | 229 +++ Makefile | 29 + NOTICE | 8 + README.md | 278 ++++ abac_test.go | 201 +++ ai_api.go | 224 +++ ai_api_test.go | 253 ++++ biba_test.go | 47 + blp_test.go | 53 + casbin-logo.png | Bin 0 -> 34267 bytes config/config.go | 270 ++++ config/config_test.go | 93 ++ config/testdata/testini.ini | 47 + constant/constants.go | 34 + constraint_test.go | 336 +++++ detector/default_detector.go | 153 ++ detector/default_detector_test.go | 341 +++++ detector/detector.go | 28 + effector/default_effector.go | 111 ++ effector/effector.go | 34 + enforcer.go | 1102 +++++++++++++++ enforcer_backslash_test.go | 180 +++ enforcer_cached.go | 188 +++ enforcer_cached_b_test.go | 224 +++ enforcer_cached_gfunction_test.go | 204 +++ enforcer_cached_synced.go | 183 +++ enforcer_cached_synced_test.go | 85 ++ enforcer_cached_test.go | 76 + enforcer_context.go | 861 ++++++++++++ enforcer_context_interface.go | 99 ++ enforcer_context_test.go | 235 ++++ enforcer_distributed.go | 256 ++++ enforcer_interface.go | 184 +++ enforcer_json_test.go | 83 ++ enforcer_synced.go | 689 +++++++++ enforcer_synced_test.go | 537 +++++++ enforcer_test.go | 806 +++++++++++ enforcer_transactional.go | 124 ++ error_test.go | 288 ++++ errors/constraint_errors.go | 49 + errors/rbac_errors.go | 33 + examples/abac_model.conf | 11 + examples/abac_not_using_policy_model.conf | 11 + examples/abac_rule_effect_policy.csv | 4 + examples/abac_rule_model.conf | 11 + examples/abac_rule_policy.csv | 2 + examples/basic_inverse_policy.csv | 2 + examples/basic_model.conf | 11 + examples/basic_model_without_spaces.conf | 11 + examples/basic_policy.csv | 2 + examples/basic_with_root_model.conf | 11 + examples/basic_without_resources_model.conf | 11 + examples/basic_without_resources_policy.csv | 2 + examples/basic_without_users_model.conf | 11 + examples/basic_without_users_policy.csv | 2 + examples/biba_model.conf | 14 + examples/blp_model.conf | 14 + examples/comment_model.conf | 12 + examples/error/error_model.conf | 11 + examples/error/error_policy.csv | 2 + examples/eval_operator_model.conf | 11 + examples/eval_operator_policy.csv | 1 + examples/glob_model.conf | 11 + examples/glob_policy.csv | 4 + examples/ipmatch_model.conf | 11 + examples/ipmatch_policy.csv | 2 + examples/keyget2_model.conf | 11 + examples/keyget_model.conf | 11 + examples/keymatch2_model.conf | 11 + examples/keymatch2_policy.csv | 2 + examples/keymatch_custom_model.conf | 11 + examples/keymatch_model.conf | 11 + examples/keymatch_policy.csv | 7 + examples/keymatch_with_rbac_in_domain.conf | 14 + examples/keymatch_with_rbac_in_domain.csv | 6 + examples/lbac_model.conf | 14 + .../multiple_policy_definitions_model.conf | 19 + .../multiple_policy_definitions_policy.csv | 5 + examples/object_conditions_model.conf | 14 + examples/object_conditions_policy.csv | 5 + examples/orbac_model.conf | 16 + examples/orbac_policy.csv | 25 + examples/pbac_model.conf | 11 + examples/pbac_policy.csv | 2 + .../rbac_with_pattern_large_scale_model.conf | 14 + examples/priority_indeterminate_policy.csv | 1 + examples/priority_model.conf | 14 + examples/priority_model_enforce_context.conf | 16 + examples/priority_model_explicit.conf | 14 + .../priority_model_explicit_customized.conf | 14 + examples/priority_policy.csv | 12 + examples/priority_policy_enforce_context.csv | 12 + examples/priority_policy_explicit.csv | 12 + .../priority_policy_explicit_customized.csv | 12 + examples/rbac_model.conf | 14 + examples/rbac_model_in_multi_line.conf | 15 + examples/rbac_model_matcher_using_in_op.conf | 14 + ...bac_model_matcher_using_in_op_bracket.conf | 14 + examples/rbac_policy.csv | 5 + examples/rbac_with_abac_rule_model.conf | 14 + examples/rbac_with_abac_rule_policy.csv | 20 + examples/rbac_with_all_pattern_model.conf | 14 + examples/rbac_with_all_pattern_policy.csv | 4 + examples/rbac_with_constraints_model.conf | 20 + examples/rbac_with_cycle_policy.csv | 7 + examples/rbac_with_deny_model.conf | 14 + examples/rbac_with_deny_policy.csv | 7 + ...c_with_different_types_of_roles_model.conf | 15 + ...c_with_different_types_of_roles_policy.csv | 12 + examples/rbac_with_domain_pattern_model.conf | 14 + examples/rbac_with_domain_pattern_policy.csv | 8 + ...rbac_with_domain_temporal_roles_model.conf | 14 + ...rbac_with_domain_temporal_roles_policy.csv | 24 + ...omain_temporal_roles_transitive_policy.csv | 5 + .../rbac_with_domains_conditional_model.conf | 15 + .../rbac_with_domains_conditional_policy.csv | 13 + examples/rbac_with_domains_model.conf | 14 + examples/rbac_with_domains_policy.csv | 6 + examples/rbac_with_domains_policy2.csv | 9 + examples/rbac_with_hierarchy_policy.csv | 10 + ...bac_with_hierarchy_with_domains_policy.csv | 11 + examples/rbac_with_multiple_policy_model.conf | 17 + examples/rbac_with_multiple_policy_policy.csv | 9 + examples/rbac_with_not_deny_model.conf | 14 + examples/rbac_with_pattern_model.conf | 15 + examples/rbac_with_pattern_policy.csv | 28 + examples/rbac_with_resource_roles_model.conf | 15 + examples/rbac_with_resource_roles_policy.csv | 7 + examples/rbac_with_temporal_roles_model.conf | 14 + examples/rbac_with_temporal_roles_policy.csv | 24 + examples/rebac_model.conf | 15 + examples/rebac_policy.csv | 7 + examples/subject_priority_model.conf | 14 + .../subject_priority_model_with_domain.conf | 14 + examples/subject_priority_policy.csv | 16 + .../subject_priority_policy_with_domain.csv | 7 + examples/syntax_matcher_model.conf | 11 + filter_test.go | 212 +++ frontend.go | 60 + frontend_old.go | 33 + frontend_old_test.go | 96 ++ frontend_test.go | 69 + go.mod | 9 + go.sum | 6 + internal_api.go | 555 ++++++++ lbac_test.go | 59 + log/default_logger.go | 142 ++ log/logger.go | 29 + log/types.go | 63 + logger_test.go | 251 ++++ management_api.go | 519 +++++++ management_api_b_test.go | 225 +++ management_api_test.go | 372 +++++ model/assertion.go | 202 +++ model/constraint.go | 285 ++++ model/function.go | 69 + model/model.go | 442 ++++++ model/model_test.go | 164 +++ model/policy.go | 482 +++++++ model_b_test.go | 294 ++++ model_test.go | 834 +++++++++++ orbac_test.go | 83 ++ pbac_test.go | 69 + persist/adapter.go | 116 ++ persist/adapter_context.go | 42 + persist/adapter_filtered.go | 32 + persist/adapter_filtered_context.go | 34 + persist/batch_adapter.go | 29 + persist/batch_adapter_context.go | 32 + persist/cache/cache.go | 42 + persist/cache/cache_sync.go | 89 ++ persist/cache/default-cache.go | 72 + persist/dispatcher.go | 36 + persist/file-adapter/adapter.go | 157 +++ persist/file-adapter/adapter_context.go | 120 ++ persist/file-adapter/adapter_filtered.go | 159 +++ .../file-adapter/adapter_filtered_context.go | 60 + persist/file-adapter/adapter_mock.go | 125 ++ persist/persist_test.go | 154 ++ persist/string-adapter/adapter.go | 103 ++ persist/string-adapter/adapter_context.go | 78 + persist/string-adapter/adapter_test.go | 173 +++ persist/transaction.go | 61 + persist/update_adapter.go | 30 + persist/update_adapter_context.go | 33 + persist/watcher.go | 32 + persist/watcher_ex.go | 43 + persist/watcher_update.go | 29 + rbac/context_role_manager.go | 49 + rbac/default-role-manager/role_manager.go | 1252 +++++++++++++++++ .../default-role-manager/role_manager_test.go | 436 ++++++ rbac/role_manager.go | 83 ++ rbac_api.go | 733 ++++++++++ rbac_api_context.go | 134 ++ rbac_api_synced.go | 221 +++ rbac_api_test.go | 894 ++++++++++++ rbac_api_with_domains.go | 204 +++ rbac_api_with_domains_context.go | 116 ++ rbac_api_with_domains_synced.go | 71 + rbac_api_with_domains_test.go | 428 ++++++ rbac_with_abac_rule_test.go | 129 ++ role_manager_b_test.go | 212 +++ syntax_test.go | 37 + transaction.go | 485 +++++++ transaction_adapter_test.go | 51 + transaction_buffer.go | 134 ++ transaction_commit.go | 267 ++++ transaction_conflict.go | 137 ++ transaction_test.go | 578 ++++++++ util/builtin_operators.go | 652 +++++++++ util/builtin_operators_b_test.go | 65 + util/builtin_operators_test.go | 739 ++++++++++ util/util.go | 441 ++++++ util/util_test.go | 281 ++++ util_log.go | 169 +++ watcher_ex_test.go | 74 + watcher_test.go | 94 ++ watcher_update_test.go | 43 + 234 files changed, 28137 insertions(+) create mode 100644 .asf.yaml create mode 100644 .gitattributes create mode 100644 .github/scripts/benchmark_formatter.py create mode 100644 .github/scripts/download_artifact.js create mode 100644 .github/scripts/post_comment.js create mode 100644 .github/semantic.yml create mode 100644 .github/workflows/comment.yml create mode 100644 .github/workflows/default.yml create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/performance-pr.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .rat-excludes create mode 100644 CONTRIBUTING.md create mode 100644 DISCLAIMER create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 NOTICE create mode 100644 README.md create mode 100644 abac_test.go create mode 100644 ai_api.go create mode 100644 ai_api_test.go create mode 100644 biba_test.go create mode 100644 blp_test.go create mode 100644 casbin-logo.png create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/testdata/testini.ini create mode 100644 constant/constants.go create mode 100644 constraint_test.go create mode 100644 detector/default_detector.go create mode 100644 detector/default_detector_test.go create mode 100644 detector/detector.go create mode 100644 effector/default_effector.go create mode 100644 effector/effector.go create mode 100644 enforcer.go create mode 100644 enforcer_backslash_test.go create mode 100644 enforcer_cached.go create mode 100644 enforcer_cached_b_test.go create mode 100644 enforcer_cached_gfunction_test.go create mode 100644 enforcer_cached_synced.go create mode 100644 enforcer_cached_synced_test.go create mode 100644 enforcer_cached_test.go create mode 100644 enforcer_context.go create mode 100644 enforcer_context_interface.go create mode 100644 enforcer_context_test.go create mode 100644 enforcer_distributed.go create mode 100644 enforcer_interface.go create mode 100644 enforcer_json_test.go create mode 100644 enforcer_synced.go create mode 100644 enforcer_synced_test.go create mode 100644 enforcer_test.go create mode 100644 enforcer_transactional.go create mode 100644 error_test.go create mode 100644 errors/constraint_errors.go create mode 100644 errors/rbac_errors.go create mode 100644 examples/abac_model.conf create mode 100644 examples/abac_not_using_policy_model.conf create mode 100644 examples/abac_rule_effect_policy.csv create mode 100644 examples/abac_rule_model.conf create mode 100644 examples/abac_rule_policy.csv create mode 100644 examples/basic_inverse_policy.csv create mode 100644 examples/basic_model.conf create mode 100644 examples/basic_model_without_spaces.conf create mode 100644 examples/basic_policy.csv create mode 100644 examples/basic_with_root_model.conf create mode 100644 examples/basic_without_resources_model.conf create mode 100644 examples/basic_without_resources_policy.csv create mode 100644 examples/basic_without_users_model.conf create mode 100644 examples/basic_without_users_policy.csv create mode 100644 examples/biba_model.conf create mode 100644 examples/blp_model.conf create mode 100644 examples/comment_model.conf create mode 100644 examples/error/error_model.conf create mode 100644 examples/error/error_policy.csv create mode 100644 examples/eval_operator_model.conf create mode 100644 examples/eval_operator_policy.csv create mode 100644 examples/glob_model.conf create mode 100644 examples/glob_policy.csv create mode 100644 examples/ipmatch_model.conf create mode 100644 examples/ipmatch_policy.csv create mode 100644 examples/keyget2_model.conf create mode 100644 examples/keyget_model.conf create mode 100644 examples/keymatch2_model.conf create mode 100644 examples/keymatch2_policy.csv create mode 100644 examples/keymatch_custom_model.conf create mode 100644 examples/keymatch_model.conf create mode 100644 examples/keymatch_policy.csv create mode 100644 examples/keymatch_with_rbac_in_domain.conf create mode 100644 examples/keymatch_with_rbac_in_domain.csv create mode 100644 examples/lbac_model.conf create mode 100644 examples/multiple_policy_definitions_model.conf create mode 100644 examples/multiple_policy_definitions_policy.csv create mode 100644 examples/object_conditions_model.conf create mode 100644 examples/object_conditions_policy.csv create mode 100644 examples/orbac_model.conf create mode 100644 examples/orbac_policy.csv create mode 100644 examples/pbac_model.conf create mode 100644 examples/pbac_policy.csv create mode 100644 examples/performance/rbac_with_pattern_large_scale_model.conf create mode 100644 examples/priority_indeterminate_policy.csv create mode 100644 examples/priority_model.conf create mode 100644 examples/priority_model_enforce_context.conf create mode 100644 examples/priority_model_explicit.conf create mode 100644 examples/priority_model_explicit_customized.conf create mode 100644 examples/priority_policy.csv create mode 100644 examples/priority_policy_enforce_context.csv create mode 100644 examples/priority_policy_explicit.csv create mode 100644 examples/priority_policy_explicit_customized.csv create mode 100644 examples/rbac_model.conf create mode 100644 examples/rbac_model_in_multi_line.conf create mode 100644 examples/rbac_model_matcher_using_in_op.conf create mode 100644 examples/rbac_model_matcher_using_in_op_bracket.conf create mode 100644 examples/rbac_policy.csv create mode 100644 examples/rbac_with_abac_rule_model.conf create mode 100644 examples/rbac_with_abac_rule_policy.csv create mode 100644 examples/rbac_with_all_pattern_model.conf create mode 100644 examples/rbac_with_all_pattern_policy.csv create mode 100644 examples/rbac_with_constraints_model.conf create mode 100644 examples/rbac_with_cycle_policy.csv create mode 100644 examples/rbac_with_deny_model.conf create mode 100644 examples/rbac_with_deny_policy.csv create mode 100644 examples/rbac_with_different_types_of_roles_model.conf create mode 100644 examples/rbac_with_different_types_of_roles_policy.csv create mode 100644 examples/rbac_with_domain_pattern_model.conf create mode 100644 examples/rbac_with_domain_pattern_policy.csv create mode 100644 examples/rbac_with_domain_temporal_roles_model.conf create mode 100644 examples/rbac_with_domain_temporal_roles_policy.csv create mode 100644 examples/rbac_with_domain_temporal_roles_transitive_policy.csv create mode 100644 examples/rbac_with_domains_conditional_model.conf create mode 100644 examples/rbac_with_domains_conditional_policy.csv create mode 100644 examples/rbac_with_domains_model.conf create mode 100644 examples/rbac_with_domains_policy.csv create mode 100644 examples/rbac_with_domains_policy2.csv create mode 100644 examples/rbac_with_hierarchy_policy.csv create mode 100644 examples/rbac_with_hierarchy_with_domains_policy.csv create mode 100644 examples/rbac_with_multiple_policy_model.conf create mode 100644 examples/rbac_with_multiple_policy_policy.csv create mode 100644 examples/rbac_with_not_deny_model.conf create mode 100644 examples/rbac_with_pattern_model.conf create mode 100644 examples/rbac_with_pattern_policy.csv create mode 100644 examples/rbac_with_resource_roles_model.conf create mode 100644 examples/rbac_with_resource_roles_policy.csv create mode 100644 examples/rbac_with_temporal_roles_model.conf create mode 100644 examples/rbac_with_temporal_roles_policy.csv create mode 100644 examples/rebac_model.conf create mode 100644 examples/rebac_policy.csv create mode 100644 examples/subject_priority_model.conf create mode 100644 examples/subject_priority_model_with_domain.conf create mode 100644 examples/subject_priority_policy.csv create mode 100644 examples/subject_priority_policy_with_domain.csv create mode 100644 examples/syntax_matcher_model.conf create mode 100644 filter_test.go create mode 100644 frontend.go create mode 100644 frontend_old.go create mode 100644 frontend_old_test.go create mode 100644 frontend_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal_api.go create mode 100644 lbac_test.go create mode 100644 log/default_logger.go create mode 100644 log/logger.go create mode 100644 log/types.go create mode 100644 logger_test.go create mode 100644 management_api.go create mode 100644 management_api_b_test.go create mode 100644 management_api_test.go create mode 100644 model/assertion.go create mode 100644 model/constraint.go create mode 100644 model/function.go create mode 100644 model/model.go create mode 100644 model/model_test.go create mode 100644 model/policy.go create mode 100644 model_b_test.go create mode 100644 model_test.go create mode 100644 orbac_test.go create mode 100644 pbac_test.go create mode 100644 persist/adapter.go create mode 100644 persist/adapter_context.go create mode 100644 persist/adapter_filtered.go create mode 100644 persist/adapter_filtered_context.go create mode 100644 persist/batch_adapter.go create mode 100644 persist/batch_adapter_context.go create mode 100644 persist/cache/cache.go create mode 100644 persist/cache/cache_sync.go create mode 100644 persist/cache/default-cache.go create mode 100644 persist/dispatcher.go create mode 100644 persist/file-adapter/adapter.go create mode 100644 persist/file-adapter/adapter_context.go create mode 100644 persist/file-adapter/adapter_filtered.go create mode 100644 persist/file-adapter/adapter_filtered_context.go create mode 100644 persist/file-adapter/adapter_mock.go create mode 100644 persist/persist_test.go create mode 100644 persist/string-adapter/adapter.go create mode 100644 persist/string-adapter/adapter_context.go create mode 100644 persist/string-adapter/adapter_test.go create mode 100644 persist/transaction.go create mode 100644 persist/update_adapter.go create mode 100644 persist/update_adapter_context.go create mode 100644 persist/watcher.go create mode 100644 persist/watcher_ex.go create mode 100644 persist/watcher_update.go create mode 100644 rbac/context_role_manager.go create mode 100644 rbac/default-role-manager/role_manager.go create mode 100644 rbac/default-role-manager/role_manager_test.go create mode 100644 rbac/role_manager.go create mode 100644 rbac_api.go create mode 100644 rbac_api_context.go create mode 100644 rbac_api_synced.go create mode 100644 rbac_api_test.go create mode 100644 rbac_api_with_domains.go create mode 100644 rbac_api_with_domains_context.go create mode 100644 rbac_api_with_domains_synced.go create mode 100644 rbac_api_with_domains_test.go create mode 100644 rbac_with_abac_rule_test.go create mode 100644 role_manager_b_test.go create mode 100644 syntax_test.go create mode 100644 transaction.go create mode 100644 transaction_adapter_test.go create mode 100644 transaction_buffer.go create mode 100644 transaction_commit.go create mode 100644 transaction_conflict.go create mode 100644 transaction_test.go create mode 100644 util/builtin_operators.go create mode 100644 util/builtin_operators_b_test.go create mode 100644 util/builtin_operators_test.go create mode 100644 util/util.go create mode 100644 util/util_test.go create mode 100644 util_log.go create mode 100644 watcher_ex_test.go create mode 100644 watcher_test.go create mode 100644 watcher_update_test.go diff --git a/.asf.yaml b/.asf.yaml new file mode 100644 index 0000000..00cedfc --- /dev/null +++ b/.asf.yaml @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# For more information, see https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features. + +github: + description: >- + Apache Casbin: an authorization library that supports access control models like ACL, RBAC, ABAC. + homepage: https://casbin.apache.org/ + dependabot_alerts: true + dependabot_updates: false + +notifications: + commits: commits@casbin.apache.org + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b0954fa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Repository-only development infrastructure and contributor guidance are not +# part of the source release. +.github export-ignore +CONTRIBUTING.md export-ignore diff --git a/.github/scripts/benchmark_formatter.py b/.github/scripts/benchmark_formatter.py new file mode 100644 index 0000000..89247d1 --- /dev/null +++ b/.github/scripts/benchmark_formatter.py @@ -0,0 +1,248 @@ +import pathlib, re, sys + +try: + p = pathlib.Path("comparison.md") + if not p.exists(): + print("comparison.md not found, skipping post-processing.") + sys.exit(0) + + lines = p.read_text(encoding="utf-8").splitlines() + processed_lines = [] + in_code = False + def strip_worker_suffix(text: str) -> str: + return re.sub(r'(\S+?)-\d+(\s|$)', r'\1\2', text) + + def get_icon(diff_val: float) -> str: + if diff_val > 10: + return "🐌" + if diff_val < -10: + return "🚀" + return "➡️" + + def clean_superscripts(text: str) -> str: + return re.sub(r'[¹²³⁴⁵⁶⁷⁸⁹⁰]', '', text) + + def parse_val(token: str): + if '%' in token or '=' in token: + return None + token = clean_superscripts(token) + token = token.split('±')[0].strip() + token = token.split('(')[0].strip() + if not token: + return None + + m = re.match(r'^([-+]?\d*\.?\d+)([a-zA-Zµ]+)?$', token) + if not m: + return None + try: + val = float(m.group(1)) + except ValueError: + return None + suffix = (m.group(2) or "").replace("µ", "u") + multipliers = { + "n": 1e-9, + "ns": 1e-9, + "u": 1e-6, + "us": 1e-6, + "m": 1e-3, + "ms": 1e-3, + "s": 1.0, + "k": 1e3, + "K": 1e3, + "M": 1e6, + "G": 1e9, + "Ki": 1024.0, + "Mi": 1024.0**2, + "Gi": 1024.0**3, + "Ti": 1024.0**4, + "B": 1.0, + "B/op": 1.0, + "C": 1.0, # tolerate degree/unit markers that don't affect ratio + } + return val * multipliers.get(suffix, 1.0) + + def extract_two_numbers(tokens): + found = [] + for t in tokens[1:]: # skip name + if t in {"±", "∞", "~", "│", "│"}: + continue + if '%' in t or '=' in t: + continue + val = parse_val(t) + if val is not None: + found.append(val) + if len(found) == 2: + break + return found + + # Pass 0: + # 1. find a header line with pipes to derive alignment hint + # 2. calculate max content width to ensure right-most alignment + max_content_width = 0 + + for line in lines: + if line.strip() == "```": + in_code = not in_code + continue + if not in_code: + continue + + # Skip footnotes/meta for width calculation + if re.match(r'^\s*[¹²³⁴⁵⁶⁷⁸⁹⁰]', line) or re.search(r'need\s*>?=\s*\d+\s+samples', line): + continue + if not line.strip() or line.strip().startswith(('goos:', 'goarch:', 'pkg:', 'cpu:')): + continue + # Header lines are handled separately in Pass 1 + if '│' in line and ('vs base' in line or 'old' in line or 'new' in line): + continue + + # It's likely a data line + # Check if it has an existing percentage we might move/align + curr_line = strip_worker_suffix(line).rstrip() + pct_match = re.search(r'([+-]?\d+\.\d+)%', curr_line) + if pct_match: + # If we are going to realign this, we count width up to the percentage + w = len(curr_line[:pct_match.start()].rstrip()) + else: + w = len(curr_line) + + if w > max_content_width: + max_content_width = w + + # Calculate global alignment target for Diff column + # Ensure target column is beyond the longest line with some padding + diff_col_start = max_content_width + 4 + + # Calculate right boundary (pipe) position + # Diff column width ~12 chars (e.g. "+100.00% 🚀") + right_boundary = diff_col_start + 14 + + # Reset code fence tracking state for Pass 1 + in_code = False + for line in lines: + + if line.strip() == "```": + in_code = not in_code + processed_lines.append(line) + continue + + if not in_code: + processed_lines.append(line) + continue + + # footnotes keep untouched + if re.match(r'^\s*[¹²³⁴⁵⁶⁷⁸⁹⁰]', line) or re.search(r'need\s*>?=\s*\d+\s+samples', line): + processed_lines.append(line) + continue + + # header lines: ensure last column labeled Diff and force alignment + if '│' in line and ('vs base' in line or 'old' in line or 'new' in line): + # Strip trailing pipe and whitespace + stripped_header = line.rstrip().rstrip('│').rstrip() + + # If "vs base" is present, ensure we don't duplicate "Diff" if it's already there + # But we want to enforce OUR alignment, so we might strip existing Diff + stripped_header = re.sub(r'\s+Diff\s*$', '', stripped_header, flags=re.IGNORECASE) + stripped_header = re.sub(r'\s+Delta\b', '', stripped_header, flags=re.IGNORECASE) + + # Pad to diff_col_start + if len(stripped_header) < diff_col_start: + new_header = stripped_header + " " * (diff_col_start - len(stripped_header)) + else: + new_header = stripped_header + " " + + # Add Diff column header if it's the second header row (vs base) + if 'vs base' in line: + new_header += "Diff" + + # Add closing pipe at the right boundary + current_len = len(new_header) + if current_len < right_boundary: + new_header += " " * (right_boundary - current_len) + + new_header += "│" + processed_lines.append(new_header) + continue + + # non-data meta lines + if not line.strip() or line.strip().startswith(('goos:', 'goarch:', 'pkg:')): + processed_lines.append(line) + continue + + line = strip_worker_suffix(line) + tokens = line.split() + if not tokens: + processed_lines.append(line) + continue + + numbers = extract_two_numbers(tokens) + pct_match = re.search(r'([+-]?\d+\.\d+)%', line) + + # Helper to align and append + def append_aligned(left_part, content): + if len(left_part) < diff_col_start: + aligned = left_part + " " * (diff_col_start - len(left_part)) + else: + aligned = left_part + " " + + # Ensure content doesn't exceed right boundary (visual check only, we don't truncate) + # But users asked not to exceed header pipe. + # Header pipe is at right_boundary. + # Content starts at diff_col_start. + # So content length should be <= right_boundary - diff_col_start + return f"{aligned}{content}" + + # Special handling for geomean when values missing or zero + is_geomean = tokens[0] == "geomean" + if is_geomean and (len(numbers) < 2 or any(v == 0 for v in numbers)) and not pct_match: + leading = re.match(r'^\s*', line).group(0) + left = f"{leading}geomean" + processed_lines.append(append_aligned(left, "n/a (has zero)")) + continue + + # when both values are zero, force diff = 0 and align + if len(numbers) == 2 and numbers[0] == 0 and numbers[1] == 0: + diff_val = 0.0 + icon = get_icon(diff_val) + left = line.rstrip() + processed_lines.append(append_aligned(left, f"{diff_val:+.2f}% {icon}")) + continue + + # recompute diff when we have two numeric values + if len(numbers) == 2 and numbers[0] != 0: + diff_val = (numbers[1] - numbers[0]) / numbers[0] * 100 + icon = get_icon(diff_val) + + left = line + if pct_match: + left = line[:pct_match.start()].rstrip() + else: + left = line.rstrip() + + processed_lines.append(append_aligned(left, f"{diff_val:+.2f}% {icon}")) + continue + + # fallback: align existing percentage to Diff column and (re)append icon + if pct_match: + try: + pct_val = float(pct_match.group(1)) + icon = get_icon(pct_val) + + left = line[:pct_match.start()].rstrip() + suffix = line[pct_match.end():] + # Remove any existing icon after the percentage to avoid duplicates + suffix = re.sub(r'\s*(🐌|🚀|➡️)', '', suffix) + + processed_lines.append(append_aligned(left, f"{pct_val:+.2f}% {icon}{suffix}")) + except ValueError: + processed_lines.append(line) + continue + + # If we cannot parse numbers or percentages, keep the original (only worker suffix stripped) + processed_lines.append(line) + + p.write_text("\n".join(processed_lines) + "\n", encoding="utf-8") + +except Exception as e: + print(f"Error post-processing comparison.md: {e}") + sys.exit(1) diff --git a/.github/scripts/download_artifact.js b/.github/scripts/download_artifact.js new file mode 100644 index 0000000..a3fddde --- /dev/null +++ b/.github/scripts/download_artifact.js @@ -0,0 +1,32 @@ +module.exports = async ({github, context, core}) => { + try { + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + const matchArtifact = artifacts.data.artifacts.find((artifact) => { + return artifact.name == "benchmark-results"; + }); + + if (!matchArtifact) { + core.setFailed("No artifact named 'benchmark-results' found."); + return; + } + + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + + const fs = require('fs'); + const path = require('path'); + const workspace = process.env.GITHUB_WORKSPACE; + fs.writeFileSync(path.join(workspace, 'benchmark-results.zip'), Buffer.from(download.data)); + } catch (error) { + core.setFailed(`Failed to download artifact: ${error.message}`); + } +}; diff --git a/.github/scripts/post_comment.js b/.github/scripts/post_comment.js new file mode 100644 index 0000000..e7c0a10 --- /dev/null +++ b/.github/scripts/post_comment.js @@ -0,0 +1,59 @@ +module.exports = async ({github, context, core}) => { + const fs = require('fs'); + + // Validate pr_number.txt + if (!fs.existsSync('pr_number.txt')) { + core.setFailed("Required artifact file 'pr_number.txt' was not found in the workspace."); + return; + } + const prNumberContent = fs.readFileSync('pr_number.txt', 'utf8').trim(); + const issue_number = parseInt(prNumberContent, 10); + if (!Number.isFinite(issue_number) || issue_number <= 0) { + core.setFailed('Invalid PR number in pr_number.txt: "' + prNumberContent + '"'); + return; + } + + // Validate comparison.md + if (!fs.existsSync('comparison.md')) { + core.setFailed("Required artifact file 'comparison.md' was not found in the workspace."); + return; + } + let comparison; + try { + comparison = fs.readFileSync('comparison.md', 'utf8'); + } catch (error) { + core.setFailed("Failed to read 'comparison.md': " + error.message); + return; + } + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Benchmark Comparison') + ); + + const footer = '🤖 This comment will be automatically updated with the latest benchmark results.'; + const commentBody = `${comparison}\n\n${footer}`; + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + body: commentBody + }); + } +}; diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 0000000..605f6a7 --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,2 @@ +# Always validate the PR title AND all the commits +titleAndCommits: true \ No newline at end of file diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml new file mode 100644 index 0000000..feb02e3 --- /dev/null +++ b/.github/workflows/comment.yml @@ -0,0 +1,37 @@ +name: Post Benchmark Comment + +on: + workflow_run: + workflows: ["Performance Comparison for Pull Requests"] + types: + - completed + +permissions: + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: 'Download artifact' + uses: actions/github-script@v7 + with: + script: | + const script = require('./.github/scripts/download_artifact.js') + await script({github, context, core}) + + - name: 'Unzip artifact' + run: unzip benchmark-results.zip + + - name: 'Post comment' + uses: actions/github-script@v7 + with: + script: | + const script = require('./.github/scripts/post_comment.js') + await script({github, context, core}) diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml new file mode 100644 index 0000000..f9ab698 --- /dev/null +++ b/.github/workflows/default.yml @@ -0,0 +1,44 @@ +name: Build + +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + go: ['1.21'] + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Run go test + run: make test + + benchmark: + runs-on: ubuntu-latest + strategy: + matrix: + go: ['1.21'] + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Run go test bench + run: make benchmark diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..9b9a94f --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,26 @@ +name: golangci-lint + +on: + push: + branches: + - master + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + golangci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 \ No newline at end of file diff --git a/.github/workflows/performance-pr.yml b/.github/workflows/performance-pr.yml new file mode 100644 index 0000000..21819ac --- /dev/null +++ b/.github/workflows/performance-pr.yml @@ -0,0 +1,97 @@ +name: Performance Comparison for Pull Requests + +on: + pull_request: + branches: [master] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + benchmark-pr: + name: Performance benchmark comparison + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + + # Save commit SHAs for display + - name: Save commit info + id: commits + run: | + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + echo "base_short=${BASE_SHA:0:7}" >> $GITHUB_OUTPUT + echo "head_short=${HEAD_SHA:0:7}" >> $GITHUB_OUTPUT + + # Run benchmark on PR branch + - name: Run benchmark on PR branch + run: | + go test -bench '.' -benchtime=2s -benchmem ./... | tee pr-bench.txt + + # Checkout base branch and run benchmark + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + clean: false + path: base + + - name: Run benchmark on base branch + working-directory: base + run: | + go test -bench '.' -benchtime=2s -benchmem ./... | \ + tee ../base-bench.txt + + # Install benchstat for comparison + - name: Install benchstat + run: go install golang.org/x/perf/cmd/benchstat@latest + + # Compare benchmarks using benchstat + - name: Compare benchmarks with benchstat + id: benchstat + run: | + cat > comparison.md << 'EOF' + ## Benchmark Comparison + + Comparing base branch (`${{ steps.commits.outputs.base_short }}`) + vs PR branch (`${{ steps.commits.outputs.head_short }}`) + + ``` + EOF + benchstat base-bench.txt pr-bench.txt >> comparison.md || true + echo '```' >> comparison.md + + # Post-process to append percentage + emoji column (🚀 faster < -10%, 🐌 slower > +10%, otherwise ➡️) + if [ ! -f comparison.md ]; then + echo "comparison.md not found after benchstat." >&2 + exit 1 + fi + python3 .github/scripts/benchmark_formatter.py + + # Save PR number + - name: Save PR number + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + if [ -z "$PR_NUMBER" ]; then + echo "Error: Pull request number is not available in event payload." >&2 + exit 1 + fi + echo "$PR_NUMBER" > pr_number.txt + + # Upload benchmark results + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: | + comparison.md + pr_number.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..028400f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,204 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Releases are driven by manually pushed tags only. Nothing is published +# automatically when a pull request is merged. +# +# v..-rc GitHub pre-release only. +# v.. GitHub release plus the Go module proxy. + +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + if: ${{ github.repository == 'apache/casbin' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Compute release metadata + id: release + run: | + set -euo pipefail + + TAG_NAME="${GITHUB_REF_NAME}" + if [[ "${TAG_NAME}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + IS_RC="false" + elif [[ "${TAG_NAME}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)-rc[0-9]+$ ]]; then + IS_RC="true" + else + echo "::error::Unsupported release tag: ${TAG_NAME}. Expected vX.Y.Z or vX.Y.Z-rcN." + exit 1 + fi + + VERSION="${BASH_REMATCH[1]}" + BASENAME="apache-casbin-incubating-${VERSION}-src" + + # Previous release, used as the start of the change log range. + PREVIOUS_TAG="$( + { + git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true + echo "v${VERSION}" + } | + sort -V -u | + awk -v current="v${VERSION}" ' + $0 == current { + print previous + exit + } + { + previous = $0 + } + ' + )" + + { + echo "tag=${TAG_NAME}" + echo "version=${VERSION}" + echo "is_rc=${IS_RC}" + echo "basename=${BASENAME}" + echo "previous_tag=${PREVIOUS_TAG}" + } >> "${GITHUB_OUTPUT}" + + - name: Generate release notes + run: | + set -euo pipefail + + CURRENT_TAG="${{ steps.release.outputs.tag }}" + CURRENT_VERSION="${CURRENT_TAG#v}" + PREVIOUS_TAG="${{ steps.release.outputs.previous_tag }}" + RELEASE_DATE="$(date -u +%F)" + RANGE="${CURRENT_TAG}" + + if [ -n "${PREVIOUS_TAG}" ]; then + RANGE="${PREVIOUS_TAG}..${CURRENT_TAG}" + fi + + FEATURES="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(feat)(\(.+\))?: ' || true)" + FIXES="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(fix)(\(.+\))?: ' || true)" + DOCS="$(git log --pretty=format:'%s (%h)' "${RANGE}" | grep -Ei '^(docs?|doc)(\(.+\))?: ' || true)" + + { + if [ -n "${PREVIOUS_TAG}" ]; then + echo "# [${CURRENT_VERSION}](https://github.com/${GITHUB_REPOSITORY}/compare/${PREVIOUS_TAG}...${CURRENT_TAG}) (${RELEASE_DATE})" + else + echo "# ${CURRENT_VERSION} (${RELEASE_DATE})" + fi + echo + if [ "${{ steps.release.outputs.is_rc }}" = "true" ]; then + echo "Release candidate for ${{ steps.release.outputs.version }}. This is not an official release." + echo + fi + + if [ -n "${FEATURES}" ]; then + echo "## Features" + echo + printf '%s\n' "${FEATURES}" | sed 's/^/- /' + echo + fi + + if [ -n "${FIXES}" ]; then + echo "## Fixes" + echo + printf '%s\n' "${FIXES}" | sed 's/^/- /' + echo + fi + + if [ -n "${DOCS}" ]; then + echo "## Docs" + echo + printf '%s\n' "${DOCS}" | sed 's/^/- /' + echo + fi + + if [ -z "${FEATURES}${FIXES}${DOCS}" ]; then + echo "## Notes" + echo + echo "- No feat/fix/doc commits were found in this release range." + fi + } > RELEASE_NOTES.md + + - name: Create source archives + run: | + set -euo pipefail + + BASENAME="${{ steps.release.outputs.basename }}" + mkdir -p dist + git archive --format=tar.gz --prefix="${BASENAME}/" -o "dist/${BASENAME}.tar.gz" "${{ steps.release.outputs.tag }}" + git archive --format=zip --prefix="${BASENAME}/" -o "dist/${BASENAME}.zip" "${{ steps.release.outputs.tag }}" + + cd dist + for archive in "${BASENAME}.tar.gz" "${BASENAME}.zip"; do + sha512sum "${archive}" > "${archive}.sha512" + done + sha512sum --check ./*.sha512 + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + if [ "${{ steps.release.outputs.is_rc }}" = "true" ]; then + TITLE="Apache Casbin (Incubating) ${{ steps.release.outputs.version }} (release candidate ${{ steps.release.outputs.tag }})" + FLAGS=(--prerelease) + else + TITLE="Apache Casbin (Incubating) ${{ steps.release.outputs.version }}" + FLAGS=(--latest) + fi + + gh release create "${{ steps.release.outputs.tag }}" \ + --verify-tag \ + --title "${TITLE}" \ + --notes-file RELEASE_NOTES.md \ + "${FLAGS[@]}" \ + dist/* + + - name: Publish to the Go module proxy + if: ${{ steps.release.outputs.is_rc == 'false' }} + run: | + set -euo pipefail + + MODULE_PATH="$(awk '/^module /{print $2; exit}' go.mod)" + TAG="${{ steps.release.outputs.tag }}" + + # Requesting the version makes the proxy fetch and cache it, which is + # what publishes the release to the Go module ecosystem. + for attempt in 1 2 3; do + if curl --fail --silent --show-error --location \ + "https://proxy.golang.org/${MODULE_PATH}/@v/${TAG}.info"; then + echo + echo "Published ${MODULE_PATH}@${TAG} to proxy.golang.org." + exit 0 + fi + echo "Attempt ${attempt} failed, retrying in 30s..." + sleep 30 + done + + echo "::error::Failed to publish ${MODULE_PATH}@${TAG} to proxy.golang.org." + exit 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da27805 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea/ +*.iml + +# vendor files +vendor diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a332bdb --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,198 @@ +# Based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 +# This code is licensed under the terms of the MIT license https://opensource.org/license/mit +# Copyright (c) 2021 Marat Reymers + +version: "2" +linters: + default: none + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - cyclop + - dupl + - durationcheck + - errname + - exhaustive + - funlen + - gocheckcompilerdirectives + - gochecknoinits + - gochecksumtype + - gocyclo + - godot + - gomoddirectives + - gomodguard_v2 + - goprintffuncname + - gosec + - govet + - ineffassign + - loggercheck + - makezero + - mirror + - musttag + - nakedret + - nestif + - nilerr + - noctx + - nolintlint + - nosprintfhostport + - predeclared + - promlinter + - protogetter + - reassign + - revive + - rowserrcheck + - sloglint + - spancheck + - sqlclosecheck + - staticcheck + - testableexamples + - testifylint + - tparallel + - unconvert + - unused + - usestdlibvars + - wastedassign + - whitespace + settings: + cyclop: + max-complexity: 30 + package-average: 10 + errcheck: + check-type-assertions: true + exhaustive: + check: + - switch + - map + exhaustruct: + exclude: + - ^net/http.Client$ + - ^net/http.Cookie$ + - ^net/http.Request$ + - ^net/http.Response$ + - ^net/http.Server$ + - ^net/http.Transport$ + - ^net/url.URL$ + - ^os/exec.Cmd$ + - ^reflect.StructField$ + - ^github.com/Shopify/sarama.Config$ + - ^github.com/Shopify/sarama.ProducerMessage$ + - ^github.com/mitchellh/mapstructure.DecoderConfig$ + - ^github.com/prometheus/client_golang/.+Opts$ + - ^github.com/spf13/cobra.Command$ + - ^github.com/spf13/cobra.CompletionOptions$ + - ^github.com/stretchr/testify/mock.Mock$ + - ^github.com/testcontainers/testcontainers-go.+Request$ + - ^github.com/testcontainers/testcontainers-go.FromDockerfile$ + - ^golang.org/x/tools/go/analysis.Analyzer$ + - ^google.golang.org/protobuf/.+Options$ + - ^gopkg.in/yaml.v3.Node$ + funlen: + lines: 100 + statements: 50 + ignore-comments: true + gocognit: + min-complexity: 20 + gocritic: + settings: + captLocal: + paramsOnly: false + underef: + skipRecvDeref: false + gomodguard_v2: + blocked: + - module: github.com/golang/protobuf + recommendations: + - google.golang.org/protobuf + reason: see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules + - module: github.com/satori/go.uuid + recommendations: + - github.com/google/uuid + reason: satori's package is not maintained + - module: github.com/gofrs/uuid + recommendations: + - github.com/gofrs/uuid/v5 + reason: gofrs' package was not go module before v5 + govet: + disable: + - fieldalignment + enable-all: true + inamedparam: + skip-single-param: true + mnd: + ignored-functions: + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + nakedret: + max-func-lines: 0 + nolintlint: + require-explanation: true + require-specific: true + allow-no-explanation: + - funlen + - gocognit + - lll + perfsprint: + strconcat: false + revive: + rules: + - name: unused-parameter + disabled: true + rowserrcheck: + packages: + - github.com/jmoiron/sqlx + staticcheck: + checks: + - all + - -ST1003 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - godot + source: (noinspection|TODO) + - linters: + - gocritic + source: //noinspection + - linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck + path: _test\.go + - linters: + - perfsprint + text: fmt.Sprintf can be replaced with string addition + paths: + - third_party$ + - builtin$ + - examples$ +issues: + max-same-issues: 50 +formatters: + enable: + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.rat-excludes b/.rat-excludes new file mode 100644 index 0000000..d3f68fa --- /dev/null +++ b/.rat-excludes @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Parser and test fixtures whose contents must remain unchanged. +examples/** +config/testdata/** + +# Go module metadata. go.sum is generated by the Go toolchain. +go.mod +go.sum + +# Source-control metadata. +**/.gitignore + +# Third-party MIT-licensed golangci-lint configuration. Its license and +# attribution are recorded in the root LICENSE file. +**/.golangci.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cbca33f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# How to contribute + +The following is a set of guidelines for contributing to Apache Casbin and its libraries, which are hosted at [Apache Casbin organization at Github](https://github.com/apache). + +This project adheres to the [Contributor Covenant 1.2.](https://www.contributor-covenant.org/version/1/2/0/code-of-conduct.html) By participating, you are expected to uphold this code. Please report unacceptable behavior to info@casbin.com. + +## Questions + +- We do our best to have an [up-to-date documentation](https://casbin.apache.org/docs/overview) +- [Stack Overflow](https://stackoverflow.com) is the best place to start if you have a question. Please use the [casbin tag](https://stackoverflow.com/tags/casbin/info) we are actively monitoring. We encourage you to use Stack Overflow specially for Modeling Access Control Problems, in order to build a shared knowledge base. +- You can also join our [Discord](https://discord.gg/S5UjpzGZjN). + +## Reporting issues + +Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written, through bug report. + +Before raising a new issue, check our [issue list](https://github.com/apache/casbin/issues) to determine if it already contains the problem that you are facing. + +A good bug report shouldn't leave others needing to chase you for more information. Please be as detailed as possible. The following questions might serve as a template for writing a detailed report: + +What were you trying to achieve? +What are the expected results? +What are the received results? +What are the steps to reproduce the issue? +In what environment did you encounter the issue? + +Feature requests can also be submitted as issues. + +## Pull requests + +Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should remain focused in scope and avoid unrelated commits. + +Please ask first before embarking on any significant pull request (e.g. implementing new features, refactoring code etc.), otherwise you risk spending a lot of time working on something that the maintainers might not want to merge into the project. + +First add an issue to the project to discuss the improvement. Please adhere to the coding conventions used throughout the project. If in doubt, consult the [Effective Go style guide](https://golang.org/doc/effective_go.html). diff --git a/DISCLAIMER b/DISCLAIMER new file mode 100644 index 0000000..1ef6eb0 --- /dev/null +++ b/DISCLAIMER @@ -0,0 +1,5 @@ +Apache Casbin (Incubating) is an effort undergoing incubation at the Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC. + +Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision making process have stabilized in a manner consistent with other successful ASF projects. + +While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..487362a --- /dev/null +++ b/LICENSE @@ -0,0 +1,229 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +THIRD-PARTY COMPONENTS + +The .golangci.yml configuration is based on +https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 +and is licensed under the MIT License. + +MIT License + +Copyright (c) 2021 Marat Reymers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..deb5659 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +SHELL = /bin/bash + +default: lint test + +test: + go test -race -v ./... + +benchmark: + go test -bench=. + +lint: + golangci-lint run --verbose diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..dfa082b --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +Apache Casbin (Incubating) +Copyright 2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +Portions of this software were originally developed by the Casbin project. +Copyright 2017-2026 The casbin Authors. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e6aaaf --- /dev/null +++ b/README.md @@ -0,0 +1,278 @@ + + +Apache Casbin +==== + +[![Lint](https://github.com/apache/casbin/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/apache/casbin/actions/workflows/golangci-lint.yml) +[![Build](https://github.com/apache/casbin/actions/workflows/default.yml/badge.svg)](https://github.com/apache/casbin/actions/workflows/default.yml) +[![Coverage Status](https://coveralls.io/repos/github/apache/casbin/badge.svg?branch=master)](https://coveralls.io/github/apache/casbin?branch=master) +[![Godoc](https://godoc.org/github.com/apache/casbin?status.svg)](https://pkg.go.dev/github.com/casbin/casbin/v2) +[![Release](https://img.shields.io/github/release/apache/casbin.svg)](https://github.com/apache/casbin/releases/latest) +[![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN) + +**News**: still worry about how to write the correct Apache Casbin policy? ``Apache Casbin online editor`` is coming to help! Try it at: https://casbin.apache.org/editor/ + +![casbin Logo](casbin-logo.png) + +Apache Casbin is a powerful and efficient open-source access control library for Golang projects. It provides support for enforcing authorization based on various [access control models](https://en.wikipedia.org/wiki/Computer_security_model). + +## All the languages supported by Apache Casbin: + +| [![golang](https://casbin.apache.org/img/langs/golang.png)](https://github.com/apache/casbin) | [![java](https://casbin.apache.org/img/langs/java.png)](https://github.com/casbin/jcasbin) | [![nodejs](https://casbin.apache.org/img/langs/nodejs.png)](https://github.com/casbin/node-casbin) | [![php](https://casbin.apache.org/img/langs/php.png)](https://github.com/php-casbin/php-casbin) | +|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------| +| [Casbin](https://github.com/apache/casbin) | [jCasbin](https://github.com/casbin/jcasbin) | [node-Casbin](https://github.com/casbin/node-casbin) | [PHP-Casbin](https://github.com/php-casbin/php-casbin) | +| production-ready | production-ready | production-ready | production-ready | + +| [![python](https://casbin.apache.org/img/langs/python.png)](https://github.com/casbin/pycasbin) | [![dotnet](https://casbin.apache.org/img/langs/dotnet.png)](https://github.com/casbin-net/Casbin.NET) | [![c++](https://casbin.apache.org/img/langs/cpp.png)](https://github.com/casbin/casbin-cpp) | [![rust](https://casbin.apache.org/img/langs/rust.png)](https://github.com/casbin/casbin-rs) | +|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [PyCasbin](https://github.com/casbin/pycasbin) | [Casbin.NET](https://github.com/casbin-net/Casbin.NET) | [Casbin-CPP](https://github.com/casbin/casbin-cpp) | [Casbin-RS](https://github.com/casbin/casbin-rs) | +| production-ready | production-ready | production-ready | production-ready | + +## Table of contents + +- [Supported models](#supported-models) +- [How it works?](#how-it-works) +- [Features](#features) +- [Installation](#installation) +- [Documentation](#documentation) +- [Online editor](#online-editor) +- [Tutorials](#tutorials) +- [Get started](#get-started) +- [Policy management](#policy-management) +- [Policy persistence](#policy-persistence) +- [Policy consistence between multiple nodes](#policy-consistence-between-multiple-nodes) +- [Role manager](#role-manager) +- [Benchmarks](#benchmarks) +- [Examples](#examples) +- [Middlewares](#middlewares) +- [Our adopters](#our-adopters) + +## Supported models + +1. [**ACL (Access Control List)**](https://en.wikipedia.org/wiki/Access_control_list) +2. **ACL with [superuser](https://en.wikipedia.org/wiki/Superuser)** +3. **ACL without users**: especially useful for systems that don't have authentication or user log-ins. +3. **ACL without resources**: some scenarios may target for a type of resources instead of an individual resource by using permissions like ``write-article``, ``read-log``. It doesn't control the access to a specific article or log. +4. **[RBAC (Role-Based Access Control)](https://en.wikipedia.org/wiki/Role-based_access_control)** +5. **RBAC with resource roles**: both users and resources can have roles (or groups) at the same time. +6. **RBAC with domains/tenants**: users can have different role sets for different domains/tenants. +7. **[ABAC (Attribute-Based Access Control)](https://en.wikipedia.org/wiki/Attribute-Based_Access_Control)**: syntax sugar like ``resource.Owner`` can be used to get the attribute for a resource. +8. **[RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer)**: supports paths like ``/res/*``, ``/res/:id`` and HTTP methods like ``GET``, ``POST``, ``PUT``, ``DELETE``. +9. **Deny-override**: both allow and deny authorizations are supported, deny overrides the allow. +10. **Priority**: the policy rules can be prioritized like firewall rules. + +## How it works? + +In Casbin, an access control model is abstracted into a CONF file based on the **PERM metamodel (Policy, Effect, Request, Matchers)**. So switching or upgrading the authorization mechanism for a project is just as simple as modifying a configuration. You can customize your own access control model by combining the available models. For example, you can get RBAC roles and ABAC attributes together inside one model and share one set of policy rules. + +The most basic and simplest model in Casbin is ACL. ACL's model CONF is: + +```ini +# Request definition +[request_definition] +r = sub, obj, act + +# Policy definition +[policy_definition] +p = sub, obj, act + +# Policy effect +[policy_effect] +e = some(where (p.eft == allow)) + +# Matchers +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act + +``` + +An example policy for ACL model is like: + +``` +p, alice, data1, read +p, bob, data2, write +``` + +It means: + +- alice can read data1 +- bob can write data2 + +We also support multi-line mode by appending '\\' in the end: + +```ini +# Matchers +[matchers] +m = r.sub == p.sub && r.obj == p.obj \ + && r.act == p.act +``` + +Further more, if you are using ABAC, you can try operator `in` like following in Casbin **golang** edition (jCasbin and Node-Casbin are not supported yet): + +```ini +# Matchers +[matchers] +m = r.obj == p.obj && r.act == p.act || r.obj in ('data2', 'data3') +``` + +But you **SHOULD** make sure that the length of the array is **MORE** than **1**, otherwise there will cause it to panic. + +For more operators, you may take a look at [govaluate](https://github.com/casbin/govaluate) + +## Features + +What Apache Casbin does: + +1. enforce the policy in the classic ``{subject, object, action}`` form or a customized form as you defined, both allow and deny authorizations are supported. +2. handle the storage of the access control model and its policy. +3. manage the role-user mappings and role-role mappings (aka role hierarchy in RBAC). +4. support built-in superuser like ``root`` or ``administrator``. A superuser can do anything without explicit permissions. +5. multiple built-in operators to support the rule matching. For example, ``keyMatch`` can map a resource key ``/foo/bar`` to the pattern ``/foo*``. + +What Apache Casbin does NOT do: + +1. authentication (aka verify ``username`` and ``password`` when a user logs in) +2. manage the list of users or roles. I believe it's more convenient for the project itself to manage these entities. Users usually have their passwords, and Apache Casbin is not designed as a password container. However, Apache Casbin stores the user-role mapping for the RBAC scenario. + +## Installation + +``` +go get github.com/casbin/casbin/v3 +``` + +## Documentation + +https://casbin.apache.org/docs/overview + +## Online editor + +You can also use the online editor (https://casbin.apache.org/editor/) to write your Casbin model and policy in your web browser. It provides functionality such as ``syntax highlighting`` and ``code completion``, just like an IDE for a programming language. + +## Tutorials + +https://casbin.apache.org/docs/tutorials + +## Get started + +1. New a Casbin enforcer with a model file and a policy file: + + ```go + e, _ := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv") + ``` + +Note: you can also initialize an enforcer with policy in DB instead of file, see [Policy-persistence](#policy-persistence) section for details. + +2. Add an enforcement hook into your code right before the access happens: + + ```go + sub := "alice" // the user that wants to access a resource. + obj := "data1" // the resource that is going to be accessed. + act := "read" // the operation that the user performs on the resource. + + if res, _ := e.Enforce(sub, obj, act); res { + // permit alice to read data1 + } else { + // deny the request, show an error + } + ``` + +3. Besides the static policy file, Apache Casbin also provides API for permission management at run-time. For example, You can get all the roles assigned to a user as below: + + ```go + roles, _ := e.GetImplicitRolesForUser(sub) + ``` + +See [Policy management APIs](#policy-management) for more usage. + +## Policy management + +Apache Casbin provides two sets of APIs to manage permissions: + +- [Management API](https://casbin.apache.org/docs/management-api): the primitive API that provides full support for Apache Casbin policy management. +- [RBAC API](https://casbin.apache.org/docs/rbac-api): a more friendly API for RBAC. This API is a subset of Management API. The RBAC users could use this API to simplify the code. + +We also provide a [web-based UI](https://casbin.apache.org/docs/admin-portal) for model management and policy management: + +![model editor](https://hsluoyz.github.io/casbin/ui_model_editor.png) + +![policy editor](https://hsluoyz.github.io/casbin/ui_policy_editor.png) + +## Policy persistence + +https://casbin.apache.org/docs/adapters + +## Policy consistence between multiple nodes + +https://casbin.apache.org/docs/watchers + +## Role manager + +https://casbin.apache.org/docs/role-managers + +## Benchmarks + +https://casbin.apache.org/docs/benchmark + +## Examples + +| Model | Model file | Policy file | +|---------------------------|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| ACL | [basic_model.conf](https://github.com/apache/casbin/blob/master/examples/basic_model.conf) | [basic_policy.csv](https://github.com/apache/casbin/blob/master/examples/basic_policy.csv) | +| ACL with superuser | [basic_model_with_root.conf](https://github.com/apache/casbin/blob/master/examples/basic_with_root_model.conf) | [basic_policy.csv](https://github.com/apache/casbin/blob/master/examples/basic_policy.csv) | +| ACL without users | [basic_model_without_users.conf](https://github.com/apache/casbin/blob/master/examples/basic_without_users_model.conf) | [basic_policy_without_users.csv](https://github.com/apache/casbin/blob/master/examples/basic_without_users_policy.csv) | +| ACL without resources | [basic_model_without_resources.conf](https://github.com/apache/casbin/blob/master/examples/basic_without_resources_model.conf) | [basic_policy_without_resources.csv](https://github.com/apache/casbin/blob/master/examples/basic_without_resources_policy.csv) | +| RBAC | [rbac_model.conf](https://github.com/apache/casbin/blob/master/examples/rbac_model.conf) | [rbac_policy.csv](https://github.com/apache/casbin/blob/master/examples/rbac_policy.csv) | +| RBAC with resource roles | [rbac_model_with_resource_roles.conf](https://github.com/apache/casbin/blob/master/examples/rbac_with_resource_roles_model.conf) | [rbac_policy_with_resource_roles.csv](https://github.com/apache/casbin/blob/master/examples/rbac_with_resource_roles_policy.csv) | +| RBAC with domains/tenants | [rbac_model_with_domains.conf](https://github.com/apache/casbin/blob/master/examples/rbac_with_domains_model.conf) | [rbac_policy_with_domains.csv](https://github.com/apache/casbin/blob/master/examples/rbac_with_domains_policy.csv) | +| ABAC | [abac_model.conf](https://github.com/apache/casbin/blob/master/examples/abac_model.conf) | N/A | +| RESTful | [keymatch_model.conf](https://github.com/apache/casbin/blob/master/examples/keymatch_model.conf) | [keymatch_policy.csv](https://github.com/apache/casbin/blob/master/examples/keymatch_policy.csv) | +| Deny-override | [rbac_model_with_deny.conf](https://github.com/apache/casbin/blob/master/examples/rbac_with_deny_model.conf) | [rbac_policy_with_deny.csv](https://github.com/apache/casbin/blob/master/examples/rbac_with_deny_policy.csv) | +| Priority | [priority_model.conf](https://github.com/apache/casbin/blob/master/examples/priority_model.conf) | [priority_policy.csv](https://github.com/apache/casbin/blob/master/examples/priority_policy.csv) | + +## Middlewares + +Authz middlewares for web frameworks: https://casbin.apache.org/docs/middlewares + +## Our adopters + +https://casbin.apache.org/docs/adopters + +## How to Contribute + +Please read the [contributing guide](CONTRIBUTING.md). + +## Contributors + +This project exists thanks to all the people who contribute. + + +## Star History + +[![Star History Chart](https://star-history.dera.page/svg?repos=apache/casbin&type=Date)](https://star-history.dera.page/#apache/casbin&Date) + +## License + +This project is licensed under the [Apache 2.0 license](LICENSE). + +## Contact + +If you have any issues or feature requests, please contact us. PR is welcomed. +- https://github.com/apache/casbin/issues +- https://discord.gg/S5UjpzGZjN diff --git a/abac_test.go b/abac_test.go new file mode 100644 index 0000000..fab47bb --- /dev/null +++ b/abac_test.go @@ -0,0 +1,201 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/casbin/casbin/v3/util" +) + +type testResource struct { + Name string + Owner string +} + +func newTestResource(name string, owner string) testResource { + r := testResource{} + r.Name = name + r.Owner = owner + return r +} + +func TestABACModel(t *testing.T) { + e, _ := NewEnforcer("examples/abac_model.conf") + + data1 := newTestResource("data1", "alice") + data2 := newTestResource("data2", "bob") + + testEnforce(t, e, "alice", data1, "read", true) + testEnforce(t, e, "alice", data1, "write", true) + testEnforce(t, e, "alice", data2, "read", false) + testEnforce(t, e, "alice", data2, "write", false) + testEnforce(t, e, "bob", data1, "read", false) + testEnforce(t, e, "bob", data1, "write", false) + testEnforce(t, e, "bob", data2, "read", true) + testEnforce(t, e, "bob", data2, "write", true) +} + +func TestABACMapRequest(t *testing.T) { + e, _ := NewEnforcer("examples/abac_model.conf") + + data1 := map[string]interface{}{ + "Name": "data1", + "Owner": "alice", + } + data2 := map[string]interface{}{ + "Name": "data2", + "Owner": "bob", + } + + testEnforce(t, e, "alice", data1, "read", true) + testEnforce(t, e, "alice", data1, "write", true) + testEnforce(t, e, "alice", data2, "read", false) + testEnforce(t, e, "alice", data2, "write", false) + testEnforce(t, e, "bob", data1, "read", false) + testEnforce(t, e, "bob", data1, "write", false) + testEnforce(t, e, "bob", data2, "read", true) + testEnforce(t, e, "bob", data2, "write", true) +} + +func TestABACTypes(t *testing.T) { + e, _ := NewEnforcer("examples/abac_model.conf") + matcher := `"moderator" IN r.sub.Roles && r.sub.Enabled == true && r.sub.Age >= 21 && r.sub.Name != "foo"` + e.GetModel()["m"]["m"].Value = util.RemoveComments(util.EscapeAssertion(matcher)) + + structRequest := struct { + Roles []interface{} + Enabled bool + Age int + Name string + }{ + Roles: []interface{}{"user", "moderator"}, + Enabled: true, + Age: 30, + Name: "alice", + } + testEnforce(t, e, structRequest, "", "", true) + + mapRequest := map[string]interface{}{ + "Roles": []interface{}{"user", "moderator"}, + "Enabled": true, + "Age": 30, + "Name": "alice", + } + testEnforce(t, e, mapRequest, nil, "", true) + + e.EnableAcceptJsonRequest(true) + jsonRequest, _ := json.Marshal(mapRequest) + testEnforce(t, e, string(jsonRequest), "", "", true) +} + +func TestABACJsonRequest(t *testing.T) { + e, _ := NewEnforcer("examples/abac_model.conf") + e.EnableAcceptJsonRequest(true) + + data1Json := `{ "Name": "data1", "Owner": "alice"}` + data2Json := `{ "Name": "data2", "Owner": "bob"}` + + testEnforce(t, e, "alice", data1Json, "read", true) + testEnforce(t, e, "alice", data1Json, "write", true) + testEnforce(t, e, "alice", data2Json, "read", false) + testEnforce(t, e, "alice", data2Json, "write", false) + testEnforce(t, e, "bob", data1Json, "read", false) + testEnforce(t, e, "bob", data1Json, "write", false) + testEnforce(t, e, "bob", data2Json, "read", true) + testEnforce(t, e, "bob", data2Json, "write", true) + + e, _ = NewEnforcer("examples/abac_not_using_policy_model.conf", "examples/abac_rule_effect_policy.csv") + e.EnableAcceptJsonRequest(true) + + testEnforce(t, e, "alice", data1Json, "read", true) + testEnforce(t, e, "alice", data1Json, "write", true) + testEnforce(t, e, "alice", data2Json, "read", false) + testEnforce(t, e, "alice", data2Json, "write", false) + + e, _ = NewEnforcer("examples/abac_rule_model.conf", "examples/abac_rule_policy.csv") + e.EnableAcceptJsonRequest(true) + sub1Json := `{"Name": "alice", "Age": 16}` + sub2Json := `{"Name": "alice", "Age": 20}` + sub3Json := `{"Name": "alice", "Age": 65}` + + testEnforce(t, e, sub1Json, "/data1", "read", false) + testEnforce(t, e, sub1Json, "/data2", "read", false) + testEnforce(t, e, sub1Json, "/data1", "write", false) + testEnforce(t, e, sub1Json, "/data2", "write", true) + testEnforce(t, e, sub2Json, "/data1", "read", true) + testEnforce(t, e, sub2Json, "/data2", "read", false) + testEnforce(t, e, sub2Json, "/data1", "write", false) + testEnforce(t, e, sub2Json, "/data2", "write", true) + testEnforce(t, e, sub3Json, "/data1", "read", true) + testEnforce(t, e, sub3Json, "/data2", "read", false) + testEnforce(t, e, sub3Json, "/data1", "write", false) + testEnforce(t, e, sub3Json, "/data2", "write", false) +} + +type testSub struct { + Name string + Age int +} + +func newTestSubject(name string, age int) testSub { + s := testSub{} + s.Name = name + s.Age = age + return s +} + +func TestABACNotUsingPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/abac_not_using_policy_model.conf", "examples/abac_rule_effect_policy.csv") + data1 := newTestResource("data1", "alice") + data2 := newTestResource("data2", "bob") + + testEnforce(t, e, "alice", data1, "read", true) + testEnforce(t, e, "alice", data1, "write", true) + testEnforce(t, e, "alice", data2, "read", false) + testEnforce(t, e, "alice", data2, "write", false) +} + +func TestABACPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/abac_rule_model.conf", "examples/abac_rule_policy.csv") + m := e.GetModel() + for sec, ast := range m { + fmt.Println(sec) + for ptype, p := range ast { + fmt.Println(ptype, p) + } + } + sub1 := newTestSubject("alice", 16) + sub2 := newTestSubject("alice", 20) + sub3 := newTestSubject("alice", 65) + + testEnforce(t, e, sub1, "/data1", "read", false) + testEnforce(t, e, sub1, "/data2", "read", false) + testEnforce(t, e, sub1, "/data1", "write", false) + testEnforce(t, e, sub1, "/data2", "write", true) + testEnforce(t, e, sub2, "/data1", "read", true) + testEnforce(t, e, sub2, "/data2", "read", false) + testEnforce(t, e, sub2, "/data1", "write", false) + testEnforce(t, e, sub2, "/data2", "write", true) + testEnforce(t, e, sub3, "/data1", "read", true) + testEnforce(t, e, sub3, "/data2", "read", false) + testEnforce(t, e, sub3, "/data1", "write", false) + testEnforce(t, e, sub3, "/data2", "write", false) +} diff --git a/ai_api.go b/ai_api.go new file mode 100644 index 0000000..c5804a3 --- /dev/null +++ b/ai_api.go @@ -0,0 +1,224 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// AIConfig contains configuration for AI API calls. +type AIConfig struct { + // Endpoint is the API endpoint (e.g., "https://api.openai.com/v1/chat/completions") + Endpoint string + // APIKey is the authentication key for the API + APIKey string + // Model is the model to use (e.g., "gpt-3.5-turbo", "gpt-4") + Model string + // Timeout for API requests (default: 30s) + Timeout time.Duration +} + +// aiMessage represents a message in the OpenAI chat format. +type aiMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// aiChatRequest represents the request to OpenAI chat completions API. +type aiChatRequest struct { + Model string `json:"model"` + Messages []aiMessage `json:"messages"` +} + +// aiChatResponse represents the response from OpenAI chat completions API. +type aiChatResponse struct { + Choices []struct { + Message aiMessage `json:"message"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error,omitempty"` +} + +// SetAIConfig sets the configuration for AI API calls. +func (e *Enforcer) SetAIConfig(config AIConfig) { + if config.Timeout == 0 { + config.Timeout = 30 * time.Second + } + e.aiConfig = config +} + +// Explain returns an AI-generated explanation of why Enforce returned a particular result. +// It calls the configured OpenAI-compatible API to generate a natural language explanation. +func (e *Enforcer) Explain(rvals ...interface{}) (string, error) { + if e.aiConfig.Endpoint == "" { + return "", errors.New("AI config not set, use SetAIConfig first") + } + + // Get enforcement result and matched rules + result, matchedRules, err := e.EnforceEx(rvals...) + if err != nil { + return "", fmt.Errorf("failed to enforce: %w", err) + } + + // Build context for AI + explainContext := e.buildExplainContext(rvals, result, matchedRules) + + // Call AI API + explanation, err := e.callAIAPI(explainContext) + if err != nil { + return "", fmt.Errorf("failed to get AI explanation: %w", err) + } + + return explanation, nil +} + +// buildExplainContext builds the context string for AI explanation. +func (e *Enforcer) buildExplainContext(rvals []interface{}, result bool, matchedRules []string) string { + var sb strings.Builder + + // Add request information + sb.WriteString("Authorization Request:\n") + fmt.Fprintf(&sb, "Subject: %v\n", rvals[0]) + if len(rvals) > 1 { + fmt.Fprintf(&sb, "Object: %v\n", rvals[1]) + } + if len(rvals) > 2 { + fmt.Fprintf(&sb, "Action: %v\n", rvals[2]) + } + fmt.Fprintf(&sb, "\nEnforcement Result: %v\n", result) + + // Add matched rules + if len(matchedRules) > 0 { + sb.WriteString("\nMatched Policy Rules:\n") + for _, rule := range matchedRules { + fmt.Fprintf(&sb, "- %s\n", rule) + } + } else { + sb.WriteString("\nNo policy rules matched.\n") + } + + // Add model information + sb.WriteString("\nAccess Control Model:\n") + if m, ok := e.model["m"]; ok { + for key, ast := range m { + fmt.Fprintf(&sb, "Matcher (%s): %s\n", key, ast.Value) + } + } + if eff, ok := e.model["e"]; ok { + for key, ast := range eff { + fmt.Fprintf(&sb, "Effect (%s): %s\n", key, ast.Value) + } + } + + // Add all policies + policies, _ := e.GetPolicy() + if len(policies) > 0 { + sb.WriteString("\nAll Policy Rules:\n") + for _, policy := range policies { + fmt.Fprintf(&sb, "- %s\n", strings.Join(policy, ", ")) + } + } + + return sb.String() +} + +// callAIAPI calls the configured AI API to get an explanation. +func (e *Enforcer) callAIAPI(explainContext string) (string, error) { + // Prepare the request + messages := []aiMessage{ + { + Role: "system", + Content: "You are an expert in access control and authorization systems. " + + "Explain why an authorization request was allowed or denied based on the " + + "provided access control model, policies, and enforcement result. " + + "Be clear, concise, and educational.", + }, + { + Role: "user", + Content: fmt.Sprintf("Please explain the following authorization decision:\n\n%s", explainContext), + }, + } + + reqBody := aiChatRequest{ + Model: e.aiConfig.Model, + Messages: messages, + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("failed to marshal request: %w", err) + } + + // Create HTTP request with context + reqCtx, cancel := context.WithTimeout(context.Background(), e.aiConfig.Timeout) + defer cancel() + + req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, e.aiConfig.Endpoint, bytes.NewBuffer(jsonData)) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+e.aiConfig.APIKey) + + // Execute request + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + // Read response + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // Parse response + var chatResp aiChatResponse + if err := json.Unmarshal(body, &chatResp); err != nil { + return "", fmt.Errorf("failed to parse response: %w", err) + } + + // Check for API errors + if chatResp.Error != nil { + return "", fmt.Errorf("API error: %s", chatResp.Error.Message) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body)) + } + + // Extract explanation + if len(chatResp.Choices) == 0 { + return "", errors.New("no response from AI") + } + + return chatResp.Choices[0].Message.Content, nil +} diff --git a/ai_api_test.go b/ai_api_test.go new file mode 100644 index 0000000..bed2c64 --- /dev/null +++ b/ai_api_test.go @@ -0,0 +1,253 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// TestExplainWithoutConfig tests that Explain returns error when config is not set. +func TestExplainWithoutConfig(t *testing.T) { + e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + if err != nil { + t.Fatal(err) + } + + _, err = e.Explain("alice", "data1", "read") + if err == nil { + t.Error("Expected error when AI config is not set") + } + if !strings.Contains(err.Error(), "AI config not set") { + t.Errorf("Expected 'AI config not set' error, got: %v", err) + } +} + +// TestExplainWithMockAPI tests Explain with a mock OpenAI-compatible API. +func TestExplainWithMockAPI(t *testing.T) { + // Create a mock server that simulates OpenAI API + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify request + if r.Method != http.MethodPost { + t.Errorf("Expected POST request, got %s", r.Method) + } + if r.Header.Get("Content-Type") != "application/json" { + t.Errorf("Expected Content-Type: application/json, got %s", r.Header.Get("Content-Type")) + } + if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + t.Errorf("Expected Bearer token in Authorization header, got %s", r.Header.Get("Authorization")) + } + + // Parse request to verify structure + var req aiChatRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Errorf("Failed to decode request: %v", err) + } + + if req.Model != "gpt-3.5-turbo" { + t.Errorf("Expected model gpt-3.5-turbo, got %s", req.Model) + } + + if len(req.Messages) != 2 { + t.Errorf("Expected 2 messages, got %d", len(req.Messages)) + } + + // Send mock response + resp := aiChatResponse{ + Choices: []struct { + Message aiMessage `json:"message"` + }{ + { + Message: aiMessage{ + Role: "assistant", + Content: "The request was allowed because alice has read permission on data1 according to the policy rule.", + }, + }, + }, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + })) + defer mockServer.Close() + + // Create enforcer + e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + if err != nil { + t.Fatal(err) + } + + // Set AI config with mock server + e.SetAIConfig(AIConfig{ + Endpoint: mockServer.URL, + APIKey: "test-api-key", + Model: "gpt-3.5-turbo", + Timeout: 5 * time.Second, + }) + + // Test explanation for allowed request + explanation, err := e.Explain("alice", "data1", "read") + if err != nil { + t.Fatalf("Failed to get explanation: %v", err) + } + + if explanation == "" { + t.Error("Expected non-empty explanation") + } + + if !strings.Contains(explanation, "allowed") { + t.Errorf("Expected explanation to mention 'allowed', got: %s", explanation) + } +} + +// TestExplainDenied tests Explain for a denied request. +func TestExplainDenied(t *testing.T) { + // Create a mock server + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := aiChatResponse{ + Choices: []struct { + Message aiMessage `json:"message"` + }{ + { + Message: aiMessage{ + Role: "assistant", + Content: "The request was denied because there is no policy rule that allows alice to write to data1.", + }, + }, + }, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + })) + defer mockServer.Close() + + // Create enforcer + e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + if err != nil { + t.Fatal(err) + } + + // Set AI config + e.SetAIConfig(AIConfig{ + Endpoint: mockServer.URL, + APIKey: "test-api-key", + Model: "gpt-3.5-turbo", + Timeout: 5 * time.Second, + }) + + // Test explanation for denied request + explanation, err := e.Explain("alice", "data1", "write") + if err != nil { + t.Fatalf("Failed to get explanation: %v", err) + } + + if explanation == "" { + t.Error("Expected non-empty explanation") + } + + if !strings.Contains(explanation, "denied") { + t.Errorf("Expected explanation to mention 'denied', got: %s", explanation) + } +} + +// TestExplainAPIError tests handling of API errors. +func TestExplainAPIError(t *testing.T) { + // Create a mock server that returns an error + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := aiChatResponse{ + Error: &struct { + Message string `json:"message"` + }{ + Message: "Invalid API key", + }, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(resp) + })) + defer mockServer.Close() + + // Create enforcer + e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + if err != nil { + t.Fatal(err) + } + + // Set AI config + e.SetAIConfig(AIConfig{ + Endpoint: mockServer.URL, + APIKey: "invalid-key", + Model: "gpt-3.5-turbo", + Timeout: 5 * time.Second, + }) + + // Test that API error is properly handled + _, err = e.Explain("alice", "data1", "read") + if err == nil { + t.Error("Expected error for API failure") + } + if !strings.Contains(err.Error(), "Invalid API key") { + t.Errorf("Expected API error message, got: %v", err) + } +} + +// TestBuildExplainContext tests the context building function. +func TestBuildExplainContext(t *testing.T) { + e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + if err != nil { + t.Fatal(err) + } + + // Test with matched rules + rvals := []interface{}{"alice", "data1", "read"} + result := true + matchedRules := []string{"alice, data1, read"} + + context := e.buildExplainContext(rvals, result, matchedRules) + + // Verify context contains expected elements + if !strings.Contains(context, "alice") { + t.Error("Context should contain subject 'alice'") + } + if !strings.Contains(context, "data1") { + t.Error("Context should contain object 'data1'") + } + if !strings.Contains(context, "read") { + t.Error("Context should contain action 'read'") + } + if !strings.Contains(context, "true") { + t.Error("Context should contain result 'true'") + } + if !strings.Contains(context, "alice, data1, read") { + t.Error("Context should contain matched rule") + } + + // Test with no matched rules + context2 := e.buildExplainContext(rvals, false, []string{}) + if !strings.Contains(context2, "No policy rules matched") { + t.Error("Context should indicate no matched rules") + } +} diff --git a/biba_test.go b/biba_test.go new file mode 100644 index 0000000..d83908e --- /dev/null +++ b/biba_test.go @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +func testEnforceBiba(t *testing.T, e *Enforcer, sub string, subLevel float64, obj string, objLevel float64, act string, res bool) { + t.Helper() + if myRes, err := e.Enforce(sub, subLevel, obj, objLevel, act); err != nil { + t.Errorf("Enforce Error: %s", err) + } else if myRes != res { + t.Errorf("%s, %v, %s, %v, %s: %t, supposed to be %t", sub, subLevel, obj, objLevel, act, myRes, res) + } +} + +func TestBibaModel(t *testing.T) { + e, _ := NewEnforcer("examples/biba_model.conf") + + testEnforceBiba(t, e, "alice", 3, "data1", 1, "read", false) + testEnforceBiba(t, e, "bob", 2, "data2", 2, "read", true) + testEnforceBiba(t, e, "charlie", 1, "data1", 1, "read", true) + testEnforceBiba(t, e, "bob", 2, "data3", 3, "read", true) + testEnforceBiba(t, e, "charlie", 1, "data2", 2, "read", true) + + testEnforceBiba(t, e, "alice", 3, "data3", 3, "write", true) + testEnforceBiba(t, e, "bob", 2, "data3", 3, "write", false) + testEnforceBiba(t, e, "charlie", 1, "data2", 2, "write", false) + testEnforceBiba(t, e, "alice", 3, "data1", 1, "write", true) + testEnforceBiba(t, e, "bob", 2, "data1", 1, "write", true) +} diff --git a/blp_test.go b/blp_test.go new file mode 100644 index 0000000..4f18f78 --- /dev/null +++ b/blp_test.go @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +func testEnforceBLP(t *testing.T, e *Enforcer, sub string, subLevel float64, obj string, objLevel float64, act string, res bool) { + t.Helper() + if myRes, err := e.Enforce(sub, subLevel, obj, objLevel, act); err != nil { + t.Errorf("Enforce Error: %s", err) + } else if myRes != res { + t.Errorf("%s, %v, %s, %v, %s: %t, supposed to be %t", sub, subLevel, obj, objLevel, act, myRes, res) + } +} + +func TestBLPModel(t *testing.T) { + e, _ := NewEnforcer("examples/blp_model.conf") + + // Read operations: subject level >= object level + testEnforceBLP(t, e, "alice", 3, "data1", 1, "read", true) + testEnforceBLP(t, e, "bob", 2, "data2", 2, "read", true) + testEnforceBLP(t, e, "charlie", 1, "data1", 1, "read", true) + + // Read violations: subject level < object level + testEnforceBLP(t, e, "bob", 2, "data3", 3, "read", false) + testEnforceBLP(t, e, "charlie", 1, "data2", 2, "read", false) + + // Write operations: subject level <= object level + testEnforceBLP(t, e, "alice", 3, "data3", 3, "write", true) + testEnforceBLP(t, e, "bob", 2, "data3", 3, "write", true) + testEnforceBLP(t, e, "charlie", 1, "data2", 2, "write", true) + + // Write violations: subject level > object level + testEnforceBLP(t, e, "alice", 3, "data1", 1, "write", false) + testEnforceBLP(t, e, "bob", 2, "data1", 1, "write", false) +} diff --git a/casbin-logo.png b/casbin-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7e5d1ecf95b4f0a47e297dc1269b2faa309837d3 GIT binary patch literal 34267 zcmeFZ1yo$iwy@i{1cHSi!QE-xU4v_|;56>;?!h6!-3bl>0>Le~g`mM5f(Mt^*}~4* z=bZoMzvsU9#vLPT4Cu*PbIz*zs%lo%?54vM`fS%xVX3&fy|7|%=BOldIvXaM}1d%YX{Qbo&4!X z6y#uNZ)WRgW@AnC-LJlZjguoE3CZ_@9p}su>wFf-FH+AZtelupQICw6is}akOzTwfQ$iK0N-n1%vxUR`#Lne_3BE ztAAOvgQJ);7{MP%|D~maikmHnQ3>Q=<796L5_1MOh4f#w-oa4`^jGivge$Q6zjWki zX8dpbe1Gz9A%I-X{*Br9C%>EhHl}{>YF;ojZV`KszN3x3ij9pW|L?<9;m^Jx5)mPy zQZutQvT<>s=4Jf5_FrS>FGnCzeMb;KcuaH91KH?-tSZdR+$=zDCU#mNCpQrIgOlGi z|K>y1#>mXr?Vo(GaI-LTGco<>;|I;Z`vA{DBYj8xpYrmf?hk&947rVM?5*@2`OU2K zO+bvc)+W4+f8G2+^I>Fii`ZD&*n_JD;%DJy{8zVsu=;JZazD3raMZUp1U(n!2j5^Y zGc)35(&q#LSq+%!LB^cM^qkDBO!NjuOic8K`kYK40}zPA*uda7h_bRjn*Wn)Q5!?2 z?=$WX*G6F1tVSS^F^eIPo`aLgn4XK3m6hIrL!XNt1Y|bmVg~B7a~T``>G}ure{wBv zZw8K*`j$WFe6RiYB8)-ITpY|mV|sS5Lwa@=R%3AOjg9G2w z?B~`u{2mec4ZqI?5SYS0-2JOs50-zJnf-GI{v3ke{r_P8@5^v81zG=}Ce-h)e$)E* zP8@8E9bNS8K|&_rG4Wqa^WPo*qs8An^D=&4JS@%rU+4n|edqtI2RIBttn4f-Ec76D z@M6f$#>z~u&jA9DNoE697NEYdp^*{$pFRBF?EzMHZdR5*!_ogQJ@D7qH8jNF!34rVq^W+QqK3yT3gy8$?IaIte3(i`Xl4fKscTx^Ey9Dla&zuReS zOx!G7zdFtFA==qkJA>^1Z{i&|s_KKcxFCB6eq(zZE24+Rk;u`8NZ;1h(#%l*dmv?W zwl?}>!}zce{@Hs!`1t>DfEt19&HiuV$`7?P{1X$=|FSm!$C32sb`1a7TJ~#F{^J(p zKkhU(W>!`mp=FxIFd5cbFyJI`yodCuZ*Psd)oXTPf8G*5eR&QGoWYX;$)>~0Uy5T^^J{L z=s7u!nGJ!=AZGBa{j*ztP0H`btlv&~|IuOYKkl@jJB$A(%irJf8vnZw62G4b|M6h{ z=VAY!5AxqrM&KNV=JA*-);XgRkb#A0!RmXq^Y?t5GC0Ez;%8@LV`1ZE{QK>{TPT>hf-KcV&A^E>hwtei7EW+b{>|v` zPyVH)#$T3P;0(iGhCe*`!|?aH@ewTsq;cbQ=y6qwV^gl2B)uR7- z-wz&t74m@MXM|t4eumP6&M#aKD1Juxh3jW1J?Q+x^?>4MgkQLRhSG!1FI*2Oen$9( z>t`rE=={R{ z_=W3dC_U)>!u5dSXM|t4eumP6&M#aKD1Juxh3jW1J?Q+x^?>4MgkQLRhSG!1FI*2O zen$9(>t`rE=={R{_=W3dC_U)>!u5dSXM|t4eumP6&M#aKD1Juxh3jW1J?Q+x^?>4MgkQLRhSG!1 zFI*2Oen$8|;zIoEi+e!U;BV`30e>wI2O35=_$z&g3?-Fh0RVS$0Khi@0JyvdzitBn zPCx))M-KqtP67b1Y~pmg#Q^|5hv%X~Dz3A8=`KkGs?EIuCjE1D2gX|Nq<*N7#KeMv zCBFWw0Yo&2Ikr_E-~3;{vd6^iwh$_HL{kjS$vL$z_5&yhqmO&k9ke*@y!T0)F~V>@ z_o@d;Z=`N|IoGfpBqy_M-ngSFNeKtSqmgvo!R5xpd!Oz_*%pXn_gn0!-z_OwUkzS& z_Q5md0Bsg{pEBymFelG{U$?Ar!=R}8hJ|8fvAA4RlCpS+5@Z60{rG17Mwx0^e@Yag#)pvE9o9BT`Tp!wVHcO zX+&lU$OWwWK5v86!&N}+0^oQ$5_M*0|~%7@8Uqm4w59|)HO`3dqhjOP@bGwh(AavRbe z@&`yu_(aiAR1v6=lOz_C$0Gvwp7p)7G&Cfyp3*l~7Zt(#20g;kxtwZZWRitthccMwbHrXTP|?t6^7`C)lElKtN(OeKWXhYFZO=&H z*>xdkkCYV`>-Q&6XP*j);|Lgra&zbRmH*pLC+@Q?-zM-ao> zY7!m!AOa;uy>R@*50Uy&(7wABgcp*Qe&nz@OpY4~3G_JId-UqnE0koP>1;A|gthSA zm}fc^1kp9N%aHv81M(Sc7N7gFR&F+j)1|=|HUw)Ac3YyKPd15%IL(fqhu=YP8vw{BoQeg4eswDaknhK9zV zCj!SbMO?jI?~rUR)|U|Co-2Igt3{DyUNrg!dLVoioXkWQ5GgOADbJBu6p8AH*QR=v zvxX&k@RSSO%8HT_ZqMtpuW;TMorDFY4e+}|61I!pPf3^tofwsPZ{g**x5?hnYdO#Q z!#v6;E-Uk@DK6f&(w^eL2|g-7HF$+eLK|2qTmWRo@qLVkdb8jc-hSZtCAH1QH#)N8 zJrCT)qe^Jg-Q#o3EZ(HKp>~!Kc%~|h{i_l4SIEWB?AF3+Li<28neA#8xm$KVuua<{y1`J z^?K_sD@M%kIn(9hrSd--*Sug(9@KpEZUz{E2@VaZufoF@(1qz>G&|RsZjhE-mr~ot z{D76p9hwj}5LycC_hWea4(NxRwpa?Z%X$zjevNvDEf6=1wh|4WgpttYpoWEmgyoTv z+MZ@Xs6W1W`Vn~(xm>&HO&A8{0;kz{o{O4_itWX6i1N#qG9VE7`Z_4$Z8_+r6Y}fT z9>o_=)QpMSG*9sMk3Ghu-a>@c>{;)vGE(xNNHTml6b6iQrEY8(Yo_3u*;qu@)z*q? zX=x4cjqn()_rFee3NCd$;%0}o3eY6Q& z0*)j;=_Mu0p~yIxh^@2bySJ6asD~T>xCpoFuO#IN72Z$NiTUCF`Aw@ZsKurG!_sCq zRs5P&r&n%dWF*I%#6-ekGtIsklguGMID+Z*A{5h$@__VxUm04~@^-g1N_bvYZ2;t= zOV#}mKIi&L=&S}RBc*Xq5^r`!ABOq%PKTwn^+t+CIdQU#Gx(d5c%L!6UIqcUZC^tx z!wV_Sua!m0erSG|jI&Cc%$1EkcXZerIwU7W6`pKqZfepU3`;TfhRH^gemiaAGD1KM zgkwVxv1`^S$Nn1hm3EmM<$RaNb_u>=WI7y^Hsg@@Ktj1tu6d&%)q16$w!)|*5Md1u z>+Y7LH1RHCam;tI4!`=VsrBP!qp_9sCECU;@_xHTcO;J?2T4-cFvQw%&D!vzuBW=t za2b-HM$N!AhZDGfpj9hRc*bsdkqMrwyvU+{Ksi=IdYl|P6TktcWWTgQ>Fj*hc;Vv8 zdrU3$ckh!$>HJ5bgJOKfhC&zw;Gl9+GjmX;VkEGn}IAjdcwwNdGv`D955yY>7&IrIf>%V%ORH~N-*eqq(hk3w>ZA98e)-6%11~37 zj^_17<8JwCabbqRmqw>n%^Kq&j3+NJ=(rIa*6X8R4~G&u1GoU8CF z)F5g7Q)@hhLTF8$ST+6SL*B-oL1#SK%JCT#1T+HVg?p5zOzmp5r?zcj-Y||LCXo?C z_mvKf!O81=&Z8N}fm@7+&#nystio~^w;Z6+(vYM`9&Qr840}p84h}z9@z~btf;=%A z@9o#iz*lt3&GEoXuWysJ_4S6IhqKmd&d~%eJ1@$kPa*hl;W_!0`oA}BTQP5!gq&>n z4zfG(OPB@|0d3-H<8DhQcrou8PUkt$0MGbLNZ2Ik4%TITBfzd72bq7JO4NI03r^>K zqn-X&)b!zT@94KvP0jMQ5%oJF=WBW zq%PR2b~SFtX(P*InO@G#;N3dmoD4Bm1xL1?q*l|onSe`SEjUh~FqQ;s6J ziu-*$%)Ed?pMcIwDLW8#z2~=8-t)w{qfTxrVsQ#y6{B&vEW61PRSsk3WUNfr6@>3W zHf~@Yh>@S*c`A-h`O^P@(={XL)YEeWB}wqGX>+xS%M(G&L`UohMnI4S$#Zd&Qs9>Q zeDR)Rc5d$eo%ijPmY=bnhpm->Co&9tM^M-BSqAuJ1^NV;QK~Jm+~=pQVWN8ppkRY1u0_5%?%AvV?io zW`!`Uo0@@fGcmI%28*zP=jHorN-5t&h4oR7j(T4U@sGE-MU`8coj~K^6t>-_I;)Y3 zEeU~swbnB>Rm6_r@#EE+N?i}=nmp(+vE0O>=KVVIFnkvXOttwSMpo7(N`XrRjxUZF z<-7bf)?XSAB&l7XyINzv=x^kTfS%>zx9dR4ePTVT-M!9pV26hOz_GHpVVcL=e!V;4 zX=D)Bm?X^&9AVXvT9J3dO^F$#c5X0I3P1m3Wa0 zXCeU!g<~t`tTVVnb<9kvVxDh46*ky<;>NJf;s$TsitRKKr8`tUIns?R0fHO9Xm<8I z;q}Sek&rHdt|I?OtPn%xyX9I<&R<=8BU>-J85>TbcoGz_!btlmmo%2#`7;hy-51Ii z`9^w&{9bV3K(6@gmen7x65b~{a^D_becQE(QQAj>_c<{oy)&GI{uDOlhrvo*e#C3G zZdu=Zh2QA=SfcX`hA<_@Sn<7BeA$H2?zA7h4pzVC>T}8W{`ha?nsV)gL2qg-Sd4g) zdk+@y$N+O8PbF5HCp8v$7({vk(%*=Wqq)ZMl`q3;b~q0#Wud={mXkr7ATN^}kpXXN zhMkXi3AHfC3D?HOc_wZppp^9*iUgwUQYkYq8eZ{D+ufI-Zw}Nalv+0^W zrD@4NduKQ^U52w)#~QV|^8C=T(xr4}9pn41bGpuI?oB$6%ia9>Z6w694O#6q3NjSc z_&}(+o#Pl|VJ@7fG`!qI;Pfp0GBh3`bITIcH)>NKXd!i`(GxXIN_LDzzqDTgdFjB|`DNp2`h(Sj(@P$7s|bz11fLtoBCto_t7K<$R`CrtN|n z?D-fUsv&>bTt`;GPbjeHO;)BKtUToX))@tK?KJMIeuqJNr|CGhCw^0HPWY3wwdHS$ zl?$2mJD|b)Gbj8&R8wI$j})u)+yMub6n=aUG*je{SN@0-t4ZN_vUS05 zOE?0?$k!^RpnK)0;f0P)d0yvdt2O6~27b}UJ{+onU%gcP?9>kX; zCsN?k185x+FwHX+lf)9%kK!57g(eM@DoKb|;E=?-XEF#$6F#14GQFHYu}f7Y0EvOf z#l*zy=IW#^=N?<#iIhPkY^!c{B69WG4kzs-O1qv{0aX#V~5Q*(9y+{eeKVyw{a-#M`pM+EPa~H6p@FulBWuSLJ@I zueAGK640={XV_Wp`+_E!PQSCWLwVdPaK}XWev3p-VATt+`2w5&c=^$oV*mmMr6CV{ zFa>z7hsF%jzN@++Xl{R9R6TZ^c;}UwV)mNXdo!|@!xBZ!ER+dqpxQiVjB3@NJ-Vv+ zB-wVzFd^L$CpM0Tb!j<-aN`@tf*{;r|B*_|!GKJ|88HbP3q(;#DG5QeMaE_ja>^MD zRE#7mtJEnXZhBd1{X&)st=Jj;r#kqett&W)>5Mj1VAIF;W0;Hz%cE{`aut=R)i`N} z4XoCNu&+GMhpCG-Jm`I#Y#&m9!^NuAJJ;Qy6nxSC{ z@(rCv$MrC00ByyGxXHI_P^g)_(H|isNb>VaO5)V%f;Zh*I&i+gA?YO=h09mzh=0(#kAIvaPWk-?QfD_ zj^a63n@mhp*3xQvl`2_$gD|V}TuTWN8eO?^*LSxOe0r-koMX3c+!i+;$rxJei%;VB z;x;A|W_`N4ZIZEkHg}L!&_Xcfq-tDUsAw$NPkL7_v)o+9A!ygC8(O~l{xReHy@Y1H z&0{sw(5m2BS6(?;SvFxRtD9t#%=Oho&xo2~?pNU42W7kr1>tP1*@W_xu1~W^KXu{Z zWQAuChgxads8b-Sike!yB-v8B*pculnim7CTB+)x`IVg`(s0oY^!T}5o-D*WKCiRz zv9B_xR7{>ELgw3Uhe&B6p!m(RWHC*f93?w`NIDTZji#N z8$9b8&)t5tNpt2(HR9dD%;sVK+JeXZVBC3Y=BBPIg!U9Q4Rw3}y87L*y^UiMy1+>k{`@kHz~twAQ9eV}_Os1BT{PB`|B*CD9DJK=`JB zuhOtYN7Q4q8gH&BTMjvER_p^S^atp2T!K1sQxA!C;bpR`ZSjlCN0Vt4R2AP#Ycf(( zXTQr2%9lj)#xTf_q68YTOUQ#GeIG4A>e z0}L@XhxL3p^CNJz{hlt1ES)~}4a*C0qTDt~N;q&!n0jq@@>(r>+g*K!xJ%s-;H14xUthf;jMMmPWdOW4fxUrtqOmRWBhVPS;?mTH9 zb$M18LF882_jxSP9@ioTyc3_(aP1tkbpPso5tc*?F8LjbxqwhA^I zojjj#&+bx8j*FdwCmK#Gl)O7%vhU5lb=H@$kC*;|41I1iybj=8hfFGe!1cjBIHz_& zoPz7TsWOzr6wq{kN{LXwqHxRZ@e0oS)o5KiKE z^Ec}Ufw2O_Jv8GPn`KVgQ_uv^8OL4%n5cJ8Rm5#94t>yWS|N*sHEaoCbO2E`bi1s? zft??ze41OC0L~g*Qr~D626+c|lTA45cP0iS2?Meb=meVUf%ASeXguY+@C6iR4y<)+ zjvKG19UW6Npi8x`*$dO`X+{Ljkz5w+C~eAK4*A2Jyx+=+R?^k&O__ZAL?2elvF~$Q z`{y*du;l~&j3aU~q)aP~^hlg^s8{%PTHIr%3HWn$OrRZB0!xo{Gu-ZAC?YC0 zZ&1=k6eS3UHX?VORkIz)RkjIi3T0C))9oh?hLIA!zLmlizYCm3$$Cx=-W}l%C?vNN zG!1ap!gv(EJ)Q2u(}WUNz(Boeg&4?Buqv|jh^)NYY%vC;i_RC$7j?hUAh2vo-{c|K zqShGPj{=Ib0$E+(llEtmvomRzY~c<8ct4kE3?)2|Z`hcdGMIZBE14ie(>GhMW|&0L zGCCS7%G!5Q#+3J@%h8rM?BKejF{|J$`Y2?E%_}?Gux9DGP*D=g*;>Dy9bEqNAV&Kj zD?@RayxmIuuXC;~(fZgI=p>LNDQl4`&;mebr#&v|lXzznmLg{9v}$nco+KFue*XMIQ%h^q zN!4Qh+>2GV6MnxPj-rcK=OFaZID|&vOF-e2aDFq^9Xc$>u>@3a$4P1VX} zT>qgOg2@8=Az*1!avO0Qnaavbyve0`c(M*TkHybRCogPUNg*(NJN=5=)5K@+qZ*iq zRCxT5+i2SlNYR|YDe}iO5++K5N%gRz@6pu#sw-TO5R(;l~BKeM=-Cai!1%v2G`(8sfdEct3YrgQKGk{!n4+xy#ah@$!?9u2zndI$Ll zWSPj}<%VkVPsz%rPS5xx8fOfev7z(6kTs6Rz&mCQ#_&!jWYMg!y!{AD&0oGKDUhcm zRX}fs?-0u#Jf!CE$}o#OQBY1`)?Y;kxPscLjN@{)ei@Ausl<~1c!PhajYz}*>a$&c3WOUZ(Z83t}_`Gdjexth$yg-5lJtG#K)kG zx-F<}-nXsi(Z#~B-VuL*IoaJ6YVkNmz%>u1>VP+7iJWKlGy$-r&`$t1!a_F(c5?hA zP0yMV^P(9ULdXlc;h8$u;58rmhY)$dNgtua0pzZ4qMvTgj@vgZ?@Go}UmRDDv21w& z7WA(3OWUs-zL)b)Ke*uRRj{i}RSb*Xdn&p-Qs6@`@psvVl_|o!u&%uG$c-(8!h(tL zanG;-`PKWG_}#<2h%|jy8iPFxm8df=fu=&bX|i>wnH0|%o*OQ2qPeHnAF(3f*iej; z;{Jp%)A%i#h$(z_E~%LVnrx#^}+NvuW)lHFxGUwE63B_Ag8_**h+X+36 zH=@)SlwxkXeyxXX6+Xjr1DWJ0RK}lk4BbY0?*lX_Gx|T%o1Gf*$VRp6gBS`Y^QgzY z;>yK8tD%XePcgGfLQiR?GF_b1k>F7mv3gh`olpQP6ok#iN%Q9BF7ASQ&w4Ml9(%%H zAlZ%rru;Ecr=MX9-up8YK}Nyx6G+O61=JeTFYzxGww+uHO5KqhK0oU>IDo5$K)YUq z09N|326B;JTz7cu%t77ZMBI-Pjhtw->3VQwKu006#@)fcM}egLW_wfROYK~*D-Tr; z`xIp<{T}H;sBN$jD%3OtM*Kng=5_rOWMo~XFjJY z?z)YXs$0Ai1*fz4a`9PAI=XtUYF>#jYSji)!Pj17b&Re&R>wZ{OtTIOFXGywp4b)5 z3+~4ItYPThT=5`iDDWuH{)sRy$FHiU5)r8}%;L=pReKii=iVw`? z@=oEBmr$3H%1>4GdT3Z4xE?8QPM++J_=JVnyw9$P_rN{$S{fx*$2te7!Ju3EST^fI zGawH>5((S&tw0`|K7g>M6I|Iv;fK8OCS(G?uT}5&3hwBx6&qOH-XYfZwHa>&q8Ebk z%y4g9u#zh;WOcyT@QhhZ720DrKH$`o);D@wq+!RF8DMA{l9)sy^^S}B%E*2@|32~N zTCDNw*w|RxxtdShxqM^9Wqu+0WXZ<{f;WQ-ICj*`WQ_H@ACg&<(`<{QTt$Pr#72#G zE$d`v@zP0_l{E}j%(+9FJu!O4a4+@<_jf+0u0ht{ZZ*-B4ab&yp`;ez$;+BkK7U4F zDtQ>O%)+&=WOkJVX^9{k)l(25G&@lQ^fE{poI__;xO|io-@U%h>#-UFX3+^@DqkFad90ybqNWIK0}9Hj+Q(pw~GF^5DWn8dHxV(ZJbZz?3l zyxvN0n|{DYm3iLYbciGh^5&3m?k}aw&lH{S+B?$p2)&3##l?|=;~-Rsl4zEpWzDhF zTvs=^GQ^G=>>^&t8*N6h%^#mR@oL}6Z@k7If#`lz9Q6r(l7@;Ut3NOeg&J$N@dJsh zVAbPEw9uqc)3&_z)&k>+3CkpJ&cgItZvyRetvZf(`*tj#Zm~4So^3}{F}%}$o2qZv zJiAzmJ2s@je(UW!weM+N00=3tip0fXMZ&h$`Voy~G4YNQUFYPjH-nc(%3g@Z7vG4a zD4O+o4?_fxHMQF=4`BB}fS|*xFb7{<7^OA6r4nL_yDU$32Sw+Vc5#LaH#fnLr^N2T z(b=Aega(ee7QjU=DxXe;(&ae4J(p}x=-hA!JVM(Fx{_WKiMDsmONOwSje@h-Ax0^V zw<38J5j*Z(MC>h)xy*iZvANv#06FQDp|5Y%C$4D3#*|8Oz9mTPP_va3g?>Vvdd0+w zAtHL19}*I>t2Nycw4QG^_A-i&D>zrvC58@1UPS8+m-6n##CgTq5!RyKWI>rEHJ6?e zp9G;h(YM!_mX^=aU#YTKeOuh3%8bgV1b)Cn(2jxke4ftlTP|>&m5KMQd)Xo3a9OHravtUqNopZK*yl6nvcMs1z8yKOp+rE^O&JENev)GMyyu7FGk+J-fGx# zM(^y?@9vK&pFc@%Gr zOC@UW-Uq?)UD-&vQS$Z*fRncu(QUbS&}tdNhLr(u*l1~MiC_q__tsnQ#kS3Kz983| zM@-bbp*vS~+^E60Q2sgr;ReaU$75+y9DG233uIM+{Snv`wa|i5UqsDme?-g-KSZ0z z^JRCn+#FK^+7cYGT+E$hXk$2!;sTT@Eo{?60USOg!If3%h-2)I(b)2e zGe0Erb4O?WDH!1?)L?*!#H%g%2?ahc?XXOTPgtHYZ{GIMaEuKgNqr@e!b91pr=8Cwk=gk-M;kaxl$nWeZ7P6;8^pe5*^T@fgV%wg3 zr{)r(in@tQTGPb-1x+QZUW5tE_r%a@4ln*n^Y+)}K7KTV7bA;yRrrqrR|SLy1)`AtE+Zee~tj>j>YW$F#XxEMw#2)7B_ z9TvGc{8UM*gyKc@X|Gz%*RLFpa7LiLb(Y>`BIPA}F544S;#}w-x(U#v7(-kxTFQ^- zeSS5wfd!ejH4;MaD} zyqw*h(n%fuAecaZ5(P0I_oc$@WX8HQ6b!pr_F^DVc7jb10kkFE2kdC^*HPYQle3xgHD$^z(Kxk>%^un4>nJJ zGLJ9gX{_Wuw;p29Mb0oG0#cUEC;7Elmu{WRmYl+a^L}+`rHRnBxGt8+C)Z}RDv#tm zFkX1DB%i%tE|Yz7jU$DO!tzCwcOv460ad6^Vb=nwRPW?rzX;0lCqf_XkLt04=7S+7 zP!3TnzSnX&9W?SN@Zyv@ls-uWexEML`uV-x_0C+~P&dDA_P66!rS+;j6oDCXF{ z*9|yCk^@Slo-GsTBBaertSn+?3ac&+hG-OpR#o}px8v@k_}$_63nZXWW(`*6hC)Q> z!E2NH_Xayhr!i7R3KmlrMRZ&=^akhkZaAlsER(Q#RSxlMD}HW*C9*BWE4RXA{``Ke ze!C7%Q`kIa|30&*D@hm1Mai{eB)&bzdxQvWVBc26FR-YhXvE=${P>zqoa9AL+88bP z<0L`MWthh|(mep`nLy@md@R09Ei&HM-bku77Ai@;;=Fl*%oBy}>=$3(p!Xyb6vT8n zf?RZynVAB29s#?Y45d%L6uxLUsgJ?lUCbuog2mWXuv;TV9#DuHSTLVHEmXV+ARvtADHp0yFg?Z)a;h083Sr9hWs>r zcD2C1N7y&aixtZ1N6oq7%PIDX`fc*aD@n8;4|a9<)*dU0n=W#ah+G+6KAUIep`LLX$Vkla}n>mnU~R*o4Z>= z0oS#Q>ZP(z-hqDq|%13ai%8&%a|%*@e7A`DhZz?B_>d#v?X!2 zW)&L>>X648klmtsq5SObK>$VlxFfn3?^=gkP%f}rQ}4OnMDPU9pJC4BaT};ri5Fn-f`L&ZK*kXy*vqOciayA(N2iEdx(q{Ep4LJn zdh@Qx9z7S~h&ZjnbGC#f4TazGdrRO0*g3jO&Qxzkgc6S?X9$Vnr6%SVpmEb)yz@;+ z1tn|)?A$}4$0Ombka=A98S6Kn8RA~zCs)*rlxdMdZWH6ykIvFzWoh2sVg+m-64INB zzwX>Z2CbpI{h|w8c{Yh+j%{k9B`M-~Hukz<+#`fXjU>zzjk%@!YK^-*-HS^%(+MKk zC?(-_PC*gk)LDaJ@2ZMe|Dgm{IxX6G#f~>uLsGC@X;I)y?wL948bntPi{5EtYGig9 z#jK|T1Y`w$B96DcicaCfj7#%jMJ;fdLX&eoq3yEn&x$KQ9t4Cjx#Ml*Dr(^;BMCk~ zrO+5LD4Pt7ly26Ld#4dh|Ak>wNJaFcE7uFB(v534uM}F7^pMF|@~=kNiA5dJxM}q_ zbG)CAaYLTUcgHSlxy~t(1cho;9XBu{1?zkd|k!^|j#tTA?ZJ|7D>lrQyb zY{p6zeB|OPc~wFTd7$e}@AKv8iGwssSNz>p((~1-mdv#y`ECMdD4ww0(sa{0+ZIdBJm;RW$$u6ORQv^+?#Ls z7Vn+UKuv@EIJ83zqU6jZ?++&rsvNJC@szw#xpM&9KG=|aP3h3cv@eDuR8fFl>*EJk z;-~AP1^SrUI7taIr8vEVM{WZliCDJXb&8cOa$=Lv+zz4S=j~ksMI@&6qpjWpH31{N zkY!6IRGIDSLd3{66hV1Tj#&znm3t&TEvs1!th4j1V(tmjy9>>`2XN#R$D<$KauMnD zwkq#RN+;IG*_<>7@DFf2zKLPJp6=fhnO~bi)O00A<93%(z`;tyqw?%-nyz+ z+4pNL824zM>vz)@7QEW=-cJ(i2urBk#h}k`N0&)e@BIK(aK>[F1lucH={v$u@` zNr)%b)rwJ(wS)VG)t70dC`w}_$D9VAP25zRm^hw15L+5aaJm^`CgZdi7`}bTJpbk5 zl%RKq6qA?l-b03o*3Dt_{-}8-(Qa~&JTHzqPA+99$GkSF-HlOuK(}6?n-~N~63dtB z-DMsBDuKT-6o`I2!^4A}TFB5zRvVJn&zzV$ zWh}TP(2bP_q94Caoe-8l?DgCNA-1ntP>m|$$zoqw}lMSfR=?GCa|FK5Op}Nx|VEC3TOWO67i|f*; z1?M}@dW3I>aX5H7%ur-TrFg3=ulx5-#_KP4qwWoLE`=nG^w9)_NwiVq-4jwUw^G_p zv}^UBW5)|6y$#;b*lG!Q!Y$lLBN!6p=U3u^4;n4s|AHyE6Z2cJGbR$0#LH?mI(3kS#xuI9X!8({1R2=W4zob zbc5DF1komsO!O2Q8g5~$GxGWHEscp9!>qFHIbRQEBos^N83Nh8T#dX=pRV#3gS=JZ zQ7Wdl{?_`P>+nyP-X62az6)X2pHO~6ULQwaqQfwzGtKiv1p{)f`lCj(d0)^4pn#<^ zWj!E_>&XI#9$!Dx6&GmM0x{2m2#uWFrj=e8X?>; z1)@|5sSQT=Oc)UcXT(6b6j$OZ?R*2}{lO9~SB<@k#xw1yqx_Me3eV{LxwD8f>gm_J z(!6u+qogY0J0X0EGUnV>mMmqf8^>4v1I06!{nbaQ8Mg{u=F%Ql8N|12QUwnlss+ZYgMg+0q{<^gw67xD(E3gseHv5{qVq?yyPbb~3d(n^s&u-D~k zy?954!cK2uy-i2PpON78Xyp?O$!csBPtpr#cvWM34aOk$K{v6I*2+Apclh$Jl2Qwr zpUogEo0kURKRZDj*j3c1awn)6kZjX{Njqs`lDRpj@XmyKlatnbX}&^-fZa2!cxd|pTt{5SUAP(~Z+JqL5x3%hvsJlG zi_-VhOTW<`VD<|$^66M@b1R}a3*&|U)~476Fn*HdW^HVl*nzFQCCxc|VhYJEl%mBrhZ2WeZ$k7^nAN0bBoP(07<9)VT z4~`z(rF7qmZ1Wu?${fL#z~El&bUr7za1%uk@v(m8Gyk>V!(>D=gF9T}9x^Cbc-g$$ z+DJ#wz-%lT7tkTfR%_=A$l%9y@)Fgt)u!}tPw=HaKZ{+@funh?_2hHL{=B~T@XXrC zv6s8Rf^Iz~(HBbC%M^*n%O`ZyOKDi}OcXe6sluTg`LstY?80+XqG}|p$Q+(ObBCNIeb;`XvF8Vrz^h zv0p}T?vjiFS+4|0+pGpi*aR0PWCZ#KMo;rUmig6zU~(u)If<}bi^_E z#0==e?-;fB?s=i<+k(O}D~p_LFKLXMtmKKxf^lNzG8@cyQv(M(YKWb7AU$Y`M2bGd zG29RN4m=6d!UDoHRha5cwwk>orM<7`28BLU(E7fT7`sD<0hYcYZaj1GqlGgoji)ND zo8TjUjz53|aL%%Dd9J(i&iFNs5iR?1Dls=PpwyG zq7X0fr99y-Fm7r4te}M@iSsQENFweRQLug2$Q_T=wyzrKaabp)$V#j3Ri0xk1xzIr z3|SZ*H8@a5C&V4ab*kLZGwSSo&D!MZl9r+_DWWkdWnT)0tndI;BJ_SUME*cYbhEgT zF1LB}isyUc zzsa4CvY6~^ZmKMqr$JjHVINvm0Rf0RKveEE>s!jV68(gYOD`xT`bn^EjB(18y?fGz0~2+70?FErN{V{>72$)-*2;@*`>N)$$ms}k@C4xq zRn_x)a#a6Pi3eA5yhrSt8pad`E2uvE6`F?9?SUWbCzjqx$342WahP*Q`P{V3=u;v4 zCIZ!zOQ9v%v(x8EMG?cKj}rRhPcvpNfixz|4ZN#(FdyJ=&{fs$#TGQ!U71TTrqB^- znV7J*!cgT<;4fYLe4rz4xttTY_3P5M5S3Ws^G@cW$0Y>L{fbH@<1_>x(0jL3Z?O z++RRRwQIL!`0#;9eJG{0{= zJGDEaj-mmw4s*s*uPnH1I!vB7ve#vHK8cOxOsr7WLyve}mCS3xf3%`hUF;tpe9sdz zEzlU9xeK=@|HA!=>dt$GoHu~S+ncv9LulTdzY=n9QT5{F5V|YM5VoZb(1q1}baoP1 z^d_Qp0nTDofNGu>#{k+O%Q-;+L3^Hz)$d&urdd;H!H$b$q$M`&TV>xVg zeF-*77-iWr*4V{enQgU-3Yzx2XB2YRYnU=AEUgt&7tq(`CB{7cVVCESBdzGFi{4a6 zi$#?^gMgsKa@QEIL~TnbLc^_KYLPNlq*^in35E(fv#gKF=_EZ(Gv(6Ig>SM$e1m^YqoIj=Z)W>|X zjET*Kna3r;H$GW=qar~bieDcmo-OlmsWfa>1TT5N$Rbg1!M|yxi>A(Fe9<8bkvj+M zvM_QxM5;d^jl5$l3O)^TI@OAg*aY%xje7FA^J4 z^Bf_qFF+7j&7#aHF(_g@!SL24kwHe|$nK`7e;GI@zN~1HV#Gm zC`ho8)>GBoKdQi+L+(mdFw2J{G@=b6Lr@GoeM){(2X=5{up)j~@%>mEj3fo;7 zigw$>E8PdS>o^3W21lk;!b!#hD+u#qpk9~OgRAA-p<<_l6RuH?TZAu~T)un&fTQL@ zRd7$vGQV`)oNW}kCEUWT{TeBX&_W%@ceY3=qr5u;G@iKjnSF(|5KR$--kOGh(t{#U za~@ijsNQ&My7slxuvQw|CCf|RZGo8-i_T*@M}461QG8p(D2I9KfZa`y+7l)89LiEe z8DUCDft-6)>`a6fA1UDHF9ASGj@)Vs0r`6p?Kj~6ItksFQW)+jcTLHNkGhleD`;r0 zgu=WRAbgEt%W^9}e(dX$-C>87opwGWlr)7$<6gvLxi8f8Flna`=O9R1x0;6eQFxhEP;<%X{&q)Fj|mSW>l4|Be+*koiF)RKSB<&!ZFY{EU2TU zT)cDqfbzUogfYxeg?to)LU=;Z_mo_R8H-8)cl;aCS3NeT(a|ah4iYY(aV)NyGsrqk zdZRB$!N8EJZI239R-!bo7Ou{S92{6Re1YN$G$Fq1i7IZm$#-THG-RU-()J5ci1!{W zF~^HWrdCTNL?tzl2As&4?W;~)3{ZX7Wxl94O@J^b<^v=(8JI!CN`+VbMj4ocyGG6~ zmvP$r;DzbfSfyyoSw*2ufylpKM{Y0Qtt`LtMZ^T;h9`+FKw)8B+Z3YxB?j-vE2x!-kju}!l^-%qGm?h7ci8uDS%W^kWcHBFfS2LxjNI2L8QAh23)mhoJQZ>@ z)I4%C@fHsy?)v_|ZfM4qc;H3qQ#O9MwDrw2sGA66p>GNTh5-k;npEz!7JYE?sorL% zX;<&RM_zv{yZyE-In;*19mSL9*ol*E zv4GQkJ&7+}YZAdHABxrvc*34-nI(XV=EWbn>I)U?9W=@|`tGUrgzncvo(xk57I{K7 zcRah1>rbueYtch1N3o&t9kp@3qK}nNqc=QU8->^v1NNIlS!7k%%Np2JxsTB0S(v`y z-GTs(6wf7}iA;&-Z()P~PcH`Ii9akEiod4<+c!*HNLK2bTH6`34i6S(&b@Ad*zN&_ zh{QRo*KneBa^6^d)kkDXLtApN#i`e}nq3zpV)1L94ptao2HOk%QNQ^aq-NkmOL6rmLi#Pjmy79J7U?^5ZzLQvV;Wz%@pkTd@X2sBh6u4A zV(dAA^DuyPHp>(%!KIB;sjwp?W7etlF&^{^fJ>* zoq(zrlNh4;aE0`ntZ4Z&v24qh!(RB&XJWZ#JykXt$4H=i4iXt;Z-}oj2t}|W5tFu5 znlJzOX4Y=m5$=W%AwmYQ&#=#f>un@uzQNZ!>Zmn++G8n75!-C3KQAOlX@O5F8?V7l zc$Dw>_w%vhnb)W54sJKCR1BDi3SXNXd}>kQj`gT+Q3fhhE`mzLhyfNV4%-esPVoON zsF1y_s7%l`z)B~YXsO-wEb-*_RqZtgepaGS*F7r=!qNpBTq*u81-A``E|Rh=W=)+) zSxIrY6GDUtF}4{1fFT7D0gmPo0bFyso2IvTgel}nT>7LahkO&;J_$Gn4MM{4mh$;!K8gjXSv>YT; zl&)zcGg%xr48{l%BE){c095Rmil_J4Bhd3{fvk{-SqRwx^07V+o$iLPn~KNuF`jf+ zmou3I@j}2{GW4O|LX1?1;j}-62oWMgh~a}4PCkey:value to the configuration. +func (c *Config) AddConfig(section string, option string, value string) bool { + if section == "" { + section = DEFAULT_SECTION + } + + if _, ok := c.data[section]; !ok { + c.data[section] = make(map[string]string) + } + + _, ok := c.data[section][option] + c.data[section][option] = value + + return !ok +} + +func (c *Config) parse(fname string) (err error) { + f, err := os.Open(fname) + if err != nil { + return err + } + defer f.Close() + + buf := bufio.NewReader(f) + return c.parseBuffer(buf) +} + +func (c *Config) parseBuffer(buf *bufio.Reader) error { + var section string + var lineNum int + var buffer bytes.Buffer + var canWrite bool + for { + if canWrite { + if err := c.write(section, lineNum, &buffer); err != nil { + return err + } else { + canWrite = false + } + } + lineNum++ + line, _, err := buf.ReadLine() + if err == io.EOF { + // force write when buffer is not flushed yet + if buffer.Len() > 0 { + if err = c.write(section, lineNum, &buffer); err != nil { + return err + } + } + break + } else if err != nil { + return err + } + + line = bytes.TrimSpace(line) + switch { + case bytes.Equal(line, []byte{}), bytes.HasPrefix(line, DEFAULT_COMMENT_SEM), + bytes.HasPrefix(line, DEFAULT_COMMENT): + canWrite = true + continue + case bytes.HasPrefix(line, []byte{'['}) && bytes.HasSuffix(line, []byte{']'}): + // force write when buffer is not flushed yet + if buffer.Len() > 0 { + if err := c.write(section, lineNum, &buffer); err != nil { + return err + } + canWrite = false + } + section = string(line[1 : len(line)-1]) + default: + var p []byte + if bytes.HasSuffix(line, DEFAULT_MULTI_LINE_SEPARATOR) { + p = bytes.TrimSpace(line[:len(line)-1]) + p = append(p, " "...) + } else { + p = line + canWrite = true + } + + end := len(p) + for i, value := range p { + if value == DEFAULT_COMMENT[0] || value == DEFAULT_COMMENT_SEM[0] { + end = i + break + } + } + if _, err := buffer.Write(p[:end]); err != nil { + return err + } + } + } + + return nil +} + +func (c *Config) write(section string, lineNum int, b *bytes.Buffer) error { + if b.Len() <= 0 { + return nil + } + + optionVal := bytes.SplitN(b.Bytes(), []byte{'='}, 2) + if len(optionVal) != 2 { + return fmt.Errorf("parse the content error : line %d , %s = ? ", lineNum, optionVal[0]) + } + option := bytes.TrimSpace(optionVal[0]) + value := bytes.TrimSpace(optionVal[1]) + c.AddConfig(section, string(option), string(value)) + + // flush buffer after adding + b.Reset() + + return nil +} + +// Bool lookups up the value using the provided key and converts the value to a bool. +func (c *Config) Bool(key string) (bool, error) { + return strconv.ParseBool(c.get(key)) +} + +// Int lookups up the value using the provided key and converts the value to a int. +func (c *Config) Int(key string) (int, error) { + return strconv.Atoi(c.get(key)) +} + +// Int64 lookups up the value using the provided key and converts the value to a int64. +func (c *Config) Int64(key string) (int64, error) { + return strconv.ParseInt(c.get(key), 10, 64) +} + +// Float64 lookups up the value using the provided key and converts the value to a float64. +func (c *Config) Float64(key string) (float64, error) { + return strconv.ParseFloat(c.get(key), 64) +} + +// String lookups up the value using the provided key and converts the value to a string. +func (c *Config) String(key string) string { + return c.get(key) +} + +// Strings lookups up the value using the provided key and converts the value to an array of string +// by splitting the string by comma. +func (c *Config) Strings(key string) []string { + v := c.get(key) + if v == "" { + return nil + } + return strings.Split(v, ",") +} + +// Set sets the value for the specific key in the Config. +func (c *Config) Set(key string, value string) error { + if len(key) == 0 { + return errors.New("key is empty") + } + + var ( + section string + option string + ) + + keys := strings.Split(strings.ToLower(key), "::") + if len(keys) >= 2 { + section = keys[0] + option = keys[1] + } else { + option = keys[0] + } + + c.AddConfig(section, option, value) + return nil +} + +// section.key or key. +func (c *Config) get(key string) string { + var ( + section string + option string + ) + + keys := strings.Split(strings.ToLower(key), "::") + if len(keys) >= 2 { + section = keys[0] + option = keys[1] + } else { + section = DEFAULT_SECTION + option = keys[0] + } + + if value, ok := c.data[section][option]; ok { + return value + } + + return "" +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..66b7454 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,93 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package config + +import ( + "testing" +) + +func TestGet(t *testing.T) { + config, cerr := NewConfig("testdata/testini.ini") + if cerr != nil { + t.Errorf("Configuration file loading failed, err:%v", cerr.Error()) + t.Fatalf("err: %v", cerr) + } + + // default::key test + if v, err := config.Bool("debug"); err != nil || !v { + t.Errorf("Get failure: expected different value for debug (expected: [%#v] got: [%#v])", true, v) + t.Fatalf("err: %v", err) + } + if v := config.String("url"); v != "act.wiki" { + t.Errorf("Get failure: expected different value for url (expected: [%#v] got: [%#v])", "act.wiki", v) + } + + // redis::key test + if v := config.Strings("redis::redis.key"); len(v) != 2 || v[0] != "push1" || v[1] != "push2" { + t.Errorf("Get failure: expected different value for redis::redis.key (expected: [%#v] got: [%#v])", "[]string{push1,push2}", v) + } + if v := config.String("mysql::mysql.dev.host"); v != "127.0.0.1" { + t.Errorf("Get failure: expected different value for mysql::mysql.dev.host (expected: [%#v] got: [%#v])", "127.0.0.1", v) + } + if v := config.String("mysql::mysql.master.host"); v != "10.0.0.1" { + t.Errorf("Get failure: expected different value for mysql::mysql.master.host (expected: [%#v] got: [%#v])", "10.0.0.1", v) + } + if v := config.String("mysql::mysql.master.user"); v != "root" { + t.Errorf("Get failure: expected different value for mysql::mysql.master.user (expected: [%#v] got: [%#v])", "root", v) + } + if v := config.String("mysql::mysql.master.pass"); v != "89dds)2$" { + t.Errorf("Get failure: expected different value for mysql::mysql.master.pass (expected: [%#v] got: [%#v])", "89dds)2$", v) + } + // math::key test + if v, err := config.Int64("math::math.i64"); err != nil || v != 64 { + t.Errorf("Get failure: expected different value for math::math.i64 (expected: [%#v] got: [%#v])", 64, v) + t.Fatalf("err: %v", err) + } + if v, err := config.Float64("math::math.f64"); err != nil || v != 64.1 { + t.Errorf("Get failure: expected different value for math::math.f64 (expected: [%#v] got: [%#v])", 64.1, v) + t.Fatalf("err: %v", err) + } + + _ = config.Set("other::key1", "new test key") + + if v := config.String("other::key1"); v != "new test key" { + t.Errorf("Get failure: expected different value for other::key1 (expected: [%#v] got: [%#v])", "new test key", v) + } + + _ = config.Set("other::key1", "test key") + + if v := config.String("multi1::name"); v != "r.sub==p.sub && r.obj==p.obj" { + t.Errorf("Get failure: expected different value for multi1::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v) + } + + if v := config.String("multi2::name"); v != "r.sub==p.sub && r.obj==p.obj" { + t.Errorf("Get failure: expected different value for multi2::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v) + } + + if v := config.String("multi3::name"); v != "r.sub==p.sub && r.obj==p.obj" { + t.Errorf("Get failure: expected different value for multi3::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v) + } + + if v := config.String("multi4::name"); v != "" { + t.Errorf("Get failure: expected different value for multi4::name (expected: [%#v] got: [%#v])", "", v) + } + + if v := config.String("multi5::name"); v != "r.sub==p.sub && r.obj==p.obj" { + t.Errorf("Get failure: expected different value for multi5::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v) + } +} diff --git a/config/testdata/testini.ini b/config/testdata/testini.ini new file mode 100644 index 0000000..cbb9bab --- /dev/null +++ b/config/testdata/testini.ini @@ -0,0 +1,47 @@ +# test config +debug = true +url = act.wiki + +; redis config +[redis] +redis.key = push1,push2 + +; mysql config +[mysql] +mysql.dev.host = 127.0.0.1 +mysql.dev.user = root +mysql.dev.pass = 123456 +mysql.dev.db = test + +mysql.master.host = 10.0.0.1 # host # 10.0.0.1 +mysql.master.user = root ; user name +mysql.master.pass = 89dds)2$#d +mysql.master.db = act + +; math config +[math] +math.i64 = 64 +math.f64 = 64.1 + +# multi-line test +[multi1] +name = r.sub==p.sub \ + && r.obj==p.obj\ + \ +[multi2] +name = r.sub==p.sub \ + && r.obj==p.obj + +[multi3] +name = r.sub==p.sub \ + && r.obj==p.obj + +[multi4] +name = \ +\ + \ + +[multi5] +name = r.sub==p.sub \ + && r.obj==p.obj\ + \ diff --git a/constant/constants.go b/constant/constants.go new file mode 100644 index 0000000..62a3134 --- /dev/null +++ b/constant/constants.go @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package constant + +const ( + ActionIndex = "act" + DomainIndex = "dom" + SubjectIndex = "sub" + ObjectIndex = "obj" + PriorityIndex = "priority" +) + +const ( + AllowOverrideEffect = "some(where (p_eft == allow))" + DenyOverrideEffect = "!some(where (p_eft == deny))" + AllowAndDenyEffect = "some(where (p_eft == allow)) && !some(where (p_eft == deny))" + PriorityEffect = "priority(p_eft) || deny" + SubjectPriorityEffect = "subjectPriority(p_eft) || deny" +) diff --git a/constraint_test.go b/constraint_test.go new file mode 100644 index 0000000..21f9a5c --- /dev/null +++ b/constraint_test.go @@ -0,0 +1,336 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "strings" + "testing" + + "github.com/casbin/casbin/v3/errors" + "github.com/casbin/casbin/v3/model" +) + +func TestConstraintSOD(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[constraint_definition] +c = sod("role1", "role2") + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + + m, err := model.NewModelFromString(modelText) + if err != nil { + t.Fatalf("Failed to create model: %v", err) + } + + e, err := NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Add a user to role1 should succeed + _, err = e.AddRoleForUser("alice", "role1") + if err != nil { + t.Fatalf("Failed to add role1 to alice: %v", err) + } + + // Add a different user to role2 should succeed + _, err = e.AddRoleForUser("bob", "role2") + if err != nil { + t.Fatalf("Failed to add role2 to bob: %v", err) + } + + // Try to add role2 to alice should fail (SOD violation) + _, err = e.AddRoleForUser("alice", "role2") + if err == nil { + t.Fatal("Expected constraint violation error, got nil") + } + if !strings.Contains(err.Error(), "constraint violation") { + t.Fatalf("Expected constraint violation error, got: %v", err) + } +} + +func TestConstraintSODMax(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[constraint_definition] +c = sodMax(["role1", "role2", "role3"], 1) + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + + m, err := model.NewModelFromString(modelText) + if err != nil { + t.Fatalf("Failed to create model: %v", err) + } + + e, err := NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Add user to one role should succeed + _, err = e.AddRoleForUser("alice", "role1") + if err != nil { + t.Fatalf("Failed to add role1 to alice: %v", err) + } + + // Try to add user to another role from the set should fail + _, err = e.AddRoleForUser("alice", "role2") + if err == nil { + t.Fatal("Expected constraint violation error, got nil") + } + if !strings.Contains(err.Error(), "constraint violation") { + t.Fatalf("Expected constraint violation error, got: %v", err) + } +} + +func TestConstraintRoleMax(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[constraint_definition] +c = roleMax("admin", 2) + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + + m, err := model.NewModelFromString(modelText) + if err != nil { + t.Fatalf("Failed to create model: %v", err) + } + + e, err := NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Add first user to admin role should succeed + _, err = e.AddRoleForUser("alice", "admin") + if err != nil { + t.Fatalf("Failed to add admin to alice: %v", err) + } + + // Add second user to admin role should succeed + _, err = e.AddRoleForUser("bob", "admin") + if err != nil { + t.Fatalf("Failed to add admin to bob: %v", err) + } + + // Try to add third user to admin role should fail (exceeds max) + _, err = e.AddRoleForUser("charlie", "admin") + if err == nil { + t.Fatal("Expected constraint violation error, got nil") + } + if !strings.Contains(err.Error(), "constraint violation") { + t.Fatalf("Expected constraint violation error, got: %v", err) + } +} + +func TestConstraintRolePre(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[constraint_definition] +c = rolePre("db_admin", "security_trained") + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + + m, err := model.NewModelFromString(modelText) + if err != nil { + t.Fatalf("Failed to create model: %v", err) + } + + e, err := NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Try to add db_admin without prerequisite should fail + _, err = e.AddRoleForUser("alice", "db_admin") + if err == nil { + t.Fatal("Expected constraint violation error, got nil") + } + if !strings.Contains(err.Error(), "constraint violation") { + t.Fatalf("Expected constraint violation error, got: %v", err) + } + + // Add prerequisite role first + _, err = e.AddRoleForUser("alice", "security_trained") + if err != nil { + t.Fatalf("Failed to add security_trained to alice: %v", err) + } + + // Now adding db_admin should succeed + _, err = e.AddRoleForUser("alice", "db_admin") + if err != nil { + t.Fatalf("Failed to add db_admin to alice: %v", err) + } +} + +func TestConstraintWithoutRBAC(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[constraint_definition] +c = sod("role1", "role2") + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act +` + + _, err := model.NewModelFromString(modelText) + if err == nil { + t.Fatal("Expected error for constraints without RBAC, got nil") + } + if err != errors.ErrConstraintRequiresRBAC { + t.Fatalf("Expected ErrConstraintRequiresRBAC, got: %v", err) + } +} + +func TestConstraintParsingError(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[constraint_definition] +c = invalidFunction("role1", "role2") + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + + _, err := model.NewModelFromString(modelText) + if err == nil { + t.Fatal("Expected parsing error for invalid constraint, got nil") + } + if !strings.Contains(err.Error(), "constraint parsing error") { + t.Fatalf("Expected constraint parsing error, got: %v", err) + } +} + +func TestConstraintRollback(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[constraint_definition] +c = sod("role1", "role2") + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + + m, err := model.NewModelFromString(modelText) + if err != nil { + t.Fatalf("Failed to create model: %v", err) + } + + e, err := NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Add alice to role1 + _, err = e.AddRoleForUser("alice", "role1") + if err != nil { + t.Fatalf("Failed to add role1 to alice: %v", err) + } + + // Try to add alice to role2 (should fail with constraint violation) + _, err = e.AddRoleForUser("alice", "role2") + if err == nil { + t.Fatal("Expected constraint violation error, got nil") + } + if !strings.Contains(err.Error(), "constraint violation") { + t.Fatalf("Expected constraint violation error, got: %v", err) + } +} diff --git a/detector/default_detector.go b/detector/default_detector.go new file mode 100644 index 0000000..2eadcdc --- /dev/null +++ b/detector/default_detector.go @@ -0,0 +1,153 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package detector + +import ( + "fmt" + "strings" + + "github.com/casbin/casbin/v3/rbac" +) + +// rangeableRM is an interface for role managers that support iterating over all role links. +// This is used to build the adjacency graph for cycle detection. +type rangeableRM interface { + Range(func(name1, name2 string, domain ...string) bool) +} + +// DefaultDetector is the default implementation of the Detector interface. +// It uses depth-first search (DFS) to detect cycles in role inheritance. +type DefaultDetector struct{} + +// NewDefaultDetector creates a new instance of DefaultDetector. +func NewDefaultDetector() *DefaultDetector { + return &DefaultDetector{} +} + +// Check checks whether the current status of the passed-in RoleManager contains logical errors (e.g., cycles in role inheritance). +// It uses DFS to traverse the role graph and detect cycles. +// Returns nil if no cycle is found, otherwise returns an error with a description of the cycle. +func (d *DefaultDetector) Check(rm rbac.RoleManager) error { + // Defensive nil check to prevent runtime panics + if rm == nil { + return fmt.Errorf("role manager cannot be nil") + } + + // Build the adjacency graph by exploring all roles + graph, err := d.buildGraph(rm) + if err != nil { + return err + } + + // Run DFS to detect cycles + visited := make(map[string]bool) + recursionStack := make(map[string]bool) + + for role := range graph { + if !visited[role] { + if cycle := d.detectCycle(role, graph, visited, recursionStack, []string{}); cycle != nil { + return fmt.Errorf("cycle detected: %s", strings.Join(cycle, " -> ")) + } + } + } + + return nil +} + +// buildGraph builds an adjacency list representation of the role inheritance graph. +// It uses the Range method (via type assertion) to iterate through all role links. +func (d *DefaultDetector) buildGraph(rm rbac.RoleManager) (graph map[string][]string, err error) { + graph = make(map[string][]string) + + // Recover from any panics during Range iteration (e.g., nil pointer dereferences) + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("RoleManager is not properly initialized: %v", r) + } + }() + + // Try to cast to a RoleManager implementation that supports Range + // This works with RoleManagerImpl and similar implementations + rrm, ok := rm.(rangeableRM) + if !ok { + // Return an error if the RoleManager doesn't support Range iteration + return nil, fmt.Errorf("RoleManager does not support Range iteration, cannot detect cycles") + } + + // Use Range method to build the graph directly + rrm.Range(func(name1, name2 string, domain ...string) bool { + // Initialize empty slice for name1 if it doesn't exist + if graph[name1] == nil { + graph[name1] = []string{} + } + // Add the link: name1 -> name2 + graph[name1] = append(graph[name1], name2) + + // Ensure name2 exists in graph even if it has no outgoing edges + if graph[name2] == nil { + graph[name2] = []string{} + } + return true + }) + return graph, nil +} + +// detectCycle performs DFS to detect cycles in the role graph. +// Returns a slice representing the cycle path if found, nil otherwise. +func (d *DefaultDetector) detectCycle( + role string, + graph map[string][]string, + visited map[string]bool, + recursionStack map[string]bool, + path []string, +) []string { + // Mark the current role as visited and add to recursion stack + visited[role] = true + recursionStack[role] = true + path = append(path, role) + + // Visit all neighbors (parent roles) + for _, neighbor := range graph[role] { + if !visited[neighbor] { + // Recursively visit unvisited neighbor + if cycle := d.detectCycle(neighbor, graph, visited, recursionStack, path); cycle != nil { + return cycle + } + } else if recursionStack[neighbor] { + // Back edge found - cycle detected + // Find where the cycle starts in the path + cycleStart := -1 + for i, p := range path { + if p == neighbor { + cycleStart = i + break + } + } + if cycleStart >= 0 { + // Build the cycle path + cyclePath := append([]string{}, path[cycleStart:]...) + cyclePath = append(cyclePath, neighbor) + return cyclePath + } + } + } + + // Remove from recursion stack before returning + recursionStack[role] = false + return nil +} diff --git a/detector/default_detector_test.go b/detector/default_detector_test.go new file mode 100644 index 0000000..add3428 --- /dev/null +++ b/detector/default_detector_test.go @@ -0,0 +1,341 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package detector + +import ( + "fmt" + "strings" + "testing" + + defaultrolemanager "github.com/casbin/casbin/v3/rbac/default-role-manager" +) + +func TestDefaultDetector_NilRoleManager(t *testing.T) { + detector := NewDefaultDetector() + err := detector.Check(nil) + + if err == nil { + t.Error("Expected error for nil role manager, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "role manager cannot be nil") { + t.Errorf("Expected error message to contain 'role manager cannot be nil', got: %s", errMsg) + } + } +} + +func TestDefaultDetector_NoCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + _ = rm.AddLink("alice", "admin") + _ = rm.AddLink("bob", "user") + _ = rm.AddLink("admin", "superuser") + + detector := NewDefaultDetector() + err := detector.Check(rm) + if err != nil { + t.Errorf("Expected no cycle, but got error: %v", err) + } +} + +func TestDefaultDetector_SimpleCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + _ = rm.AddLink("A", "B") + _ = rm.AddLink("B", "A") + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + // Should contain both A and B in the cycle + if !strings.Contains(errMsg, "A") || !strings.Contains(errMsg, "B") { + t.Errorf("Expected error message to contain both A and B, got: %s", errMsg) + } + } +} + +func TestDefaultDetector_ComplexCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + _ = rm.AddLink("A", "B") + _ = rm.AddLink("B", "C") + _ = rm.AddLink("C", "A") + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + // Should contain A, B, and C in the cycle + if !strings.Contains(errMsg, "A") || !strings.Contains(errMsg, "B") || !strings.Contains(errMsg, "C") { + t.Errorf("Expected error message to contain A, B, and C, got: %s", errMsg) + } + } +} + +func TestDefaultDetector_SelfLoop(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + _ = rm.AddLink("A", "A") + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error for self-loop, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + } +} + +func TestDefaultDetector_MultipleCycles(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + // First cycle: A -> B -> A + _ = rm.AddLink("A", "B") + _ = rm.AddLink("B", "A") + // Second cycle: C -> D -> C + _ = rm.AddLink("C", "D") + _ = rm.AddLink("D", "C") + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + } +} + +func TestDefaultDetector_DisconnectedComponents(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + // Component 1: alice -> admin -> superuser + _ = rm.AddLink("alice", "admin") + _ = rm.AddLink("admin", "superuser") + // Component 2: bob -> user + _ = rm.AddLink("bob", "user") + // Component 3: carol -> moderator + _ = rm.AddLink("carol", "moderator") + + detector := NewDefaultDetector() + err := detector.Check(rm) + if err != nil { + t.Errorf("Expected no cycle in disconnected components, but got error: %v", err) + } +} + +func TestDefaultDetector_ComplexGraphWithCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + // Build a complex graph with one cycle + _ = rm.AddLink("u1", "g1") + _ = rm.AddLink("u2", "g1") + _ = rm.AddLink("g1", "g2") + _ = rm.AddLink("g2", "g3") + _ = rm.AddLink("g3", "g1") // Creates cycle: g1 -> g2 -> g3 -> g1 + _ = rm.AddLink("u3", "g4") + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + } +} + +func TestDefaultDetector_LongCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(20) + // Create a long cycle: A -> B -> C -> D -> E -> A + _ = rm.AddLink("A", "B") + _ = rm.AddLink("B", "C") + _ = rm.AddLink("C", "D") + _ = rm.AddLink("D", "E") + _ = rm.AddLink("E", "A") + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + } +} + +func TestDefaultDetector_EmptyRoleManager(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + + detector := NewDefaultDetector() + err := detector.Check(rm) + if err != nil { + t.Errorf("Expected no error for empty role manager, but got: %v", err) + } +} + +func TestDefaultDetector_LargeGraphNoCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(100) + + // Build a large graph with no cycles: a tree structure + // Create 100 levels: u0 -> u1 -> u2 -> ... -> u99 + for i := 0; i < 99; i++ { + user := fmt.Sprintf("u%d", i) + role := fmt.Sprintf("u%d", i+1) + _ = rm.AddLink(user, role) + } + + detector := NewDefaultDetector() + err := detector.Check(rm) + if err != nil { + t.Errorf("Expected no cycle in large graph, but got error: %v", err) + } +} + +func TestDefaultDetector_LargeGraphWithCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(100) + + // Build a large graph with a cycle at the end + // Create a chain: u0 -> u1 -> u2 -> ... -> u99 -> u0 + for i := 0; i < 99; i++ { + user := fmt.Sprintf("u%d", i) + role := fmt.Sprintf("u%d", i+1) + _ = rm.AddLink(user, role) + } + // Add the cycle + _ = rm.AddLink("u99", "u0") + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error in large graph, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + } +} + +// Performance test with 10,000 roles. +func TestDefaultDetector_PerformanceLargeGraph(t *testing.T) { + if testing.Short() { + t.Skip("Skipping performance test in short mode") + } + + // Use a higher maxHierarchyLevel to support deep hierarchies + rm := defaultrolemanager.NewRoleManagerImpl(10000) + + // Build a large tree structure with 10,000 roles + // Each role has up to 3 children + numRoles := 10000 + for i := 0; i < numRoles; i++ { + role := fmt.Sprintf("r%d", i) + // Add links to create a tree structure + child1 := (i * 3) + 1 + child2 := (i * 3) + 2 + child3 := (i * 3) + 3 + if child1 < numRoles { + _ = rm.AddLink(fmt.Sprintf("r%d", child1), role) + } + if child2 < numRoles { + _ = rm.AddLink(fmt.Sprintf("r%d", child2), role) + } + if child3 < numRoles { + _ = rm.AddLink(fmt.Sprintf("r%d", child3), role) + } + } + + detector := NewDefaultDetector() + err := detector.Check(rm) + if err != nil { + t.Errorf("Expected no cycle in large performance test, but got error: %v", err) + } +} + +func TestDefaultDetector_MultipleInheritance(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + // User inherits from multiple roles + _ = rm.AddLink("alice", "admin") + _ = rm.AddLink("alice", "moderator") + _ = rm.AddLink("admin", "superuser") + _ = rm.AddLink("moderator", "user") + + detector := NewDefaultDetector() + err := detector.Check(rm) + if err != nil { + t.Errorf("Expected no cycle with multiple inheritance, but got error: %v", err) + } +} + +func TestDefaultDetector_DiamondPattern(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + // Diamond pattern: alice -> admin, alice -> moderator, admin -> superuser, moderator -> superuser + _ = rm.AddLink("alice", "admin") + _ = rm.AddLink("alice", "moderator") + _ = rm.AddLink("admin", "superuser") + _ = rm.AddLink("moderator", "superuser") + + detector := NewDefaultDetector() + err := detector.Check(rm) + if err != nil { + t.Errorf("Expected no cycle in diamond pattern, but got error: %v", err) + } +} + +func TestDefaultDetector_DiamondPatternWithCycle(t *testing.T) { + rm := defaultrolemanager.NewRoleManagerImpl(10) + // Diamond pattern with cycle: alice -> admin, alice -> moderator, admin -> superuser, moderator -> superuser, superuser -> alice + _ = rm.AddLink("alice", "admin") + _ = rm.AddLink("alice", "moderator") + _ = rm.AddLink("admin", "superuser") + _ = rm.AddLink("moderator", "superuser") + _ = rm.AddLink("superuser", "alice") // Creates cycle + + detector := NewDefaultDetector() + err := detector.Check(rm) + + if err == nil { + t.Error("Expected cycle detection error in diamond pattern with cycle, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'Cycle detected', got: %s", errMsg) + } + } +} diff --git a/detector/detector.go b/detector/detector.go new file mode 100644 index 0000000..b62d765 --- /dev/null +++ b/detector/detector.go @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package detector + +import "github.com/casbin/casbin/v3/rbac" + +// Detector defines the interface of a policy consistency checker, currently used to detect RBAC inheritance cycles. +type Detector interface { + // Check checks whether the current status of the passed-in RoleManager contains logical errors (e.g., cycles in role inheritance). + // param: rm RoleManager instance + // return: If an error is found, return a descriptive error; otherwise return nil. + Check(rm rbac.RoleManager) error +} diff --git a/effector/default_effector.go b/effector/default_effector.go new file mode 100644 index 0000000..bbef15b --- /dev/null +++ b/effector/default_effector.go @@ -0,0 +1,111 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package effector + +import ( + "errors" + + "github.com/casbin/casbin/v3/constant" +) + +// DefaultEffector is default effector for Casbin. +type DefaultEffector struct{} + +// NewDefaultEffector is the constructor for DefaultEffector. +func NewDefaultEffector() *DefaultEffector { + e := DefaultEffector{} + return &e +} + +// MergeEffects merges all matching results collected by the enforcer into a single decision. +func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches []float64, policyIndex int, policyLength int) (Effect, int, error) { + result := Indeterminate + explainIndex := -1 + + switch expr { + case constant.AllowOverrideEffect: + if matches[policyIndex] == 0 { + break + } + // only check the current policyIndex + if effects[policyIndex] == Allow { + result = Allow + explainIndex = policyIndex + break + } + case constant.DenyOverrideEffect: + // only check the current policyIndex + if matches[policyIndex] != 0 && effects[policyIndex] == Deny { + result = Deny + explainIndex = policyIndex + break + } + // if no deny rules are matched at last, then allow + if policyIndex == policyLength-1 { + result = Allow + } + case constant.AllowAndDenyEffect: + // short-circuit if matched deny rule + if matches[policyIndex] != 0 && effects[policyIndex] == Deny { + result = Deny + // set hit rule to the (first) matched deny rule + explainIndex = policyIndex + break + } + + // short-circuit some effects in the middle + if policyIndex < policyLength-1 { + // choose not to short-circuit + return result, explainIndex, nil + } + // merge all effects at last + for i, eft := range effects { + if matches[i] == 0 { + continue + } + + if eft == Allow { + result = Allow + // set hit rule to first matched allow rule + explainIndex = i + break + } + } + case constant.PriorityEffect, constant.SubjectPriorityEffect: + // reverse merge, short-circuit may be earlier + for i := len(effects) - 1; i >= 0; i-- { + if matches[i] == 0 { + continue + } + + if effects[i] != Indeterminate { + if effects[i] == Allow { + result = Allow + } else { + result = Deny + } + explainIndex = i + break + } + } + default: + return Deny, -1, errors.New("unsupported effect") + } + + return result, explainIndex, nil +} diff --git a/effector/effector.go b/effector/effector.go new file mode 100644 index 0000000..8b5c51b --- /dev/null +++ b/effector/effector.go @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package effector //nolint:cyclop // TODO + +// Effect is the result for a policy rule. +type Effect int + +// Values for policy effect. +const ( + Allow Effect = iota + Indeterminate + Deny +) + +// Effector is the interface for Casbin effectors. +type Effector interface { + // MergeEffects merges all matching results collected by the enforcer into a single decision. + MergeEffects(expr string, effects []Effect, matches []float64, policyIndex int, policyLength int) (Effect, int, error) +} diff --git a/enforcer.go b/enforcer.go new file mode 100644 index 0000000..c575534 --- /dev/null +++ b/enforcer.go @@ -0,0 +1,1102 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "errors" + "fmt" + "runtime/debug" + "strings" + "sync" + + "github.com/casbin/casbin/v3/detector" + "github.com/casbin/casbin/v3/effector" + "github.com/casbin/casbin/v3/log" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" + "github.com/casbin/casbin/v3/rbac" + defaultrolemanager "github.com/casbin/casbin/v3/rbac/default-role-manager" + "github.com/casbin/casbin/v3/util" + + "github.com/casbin/govaluate" +) + +// Enforcer is the main interface for authorization enforcement and policy management. +type Enforcer struct { + modelPath string + model model.Model + fm model.FunctionMap + eft effector.Effector + + adapter persist.Adapter + watcher persist.Watcher + dispatcher persist.Dispatcher + rmMap map[string]rbac.RoleManager + condRmMap map[string]rbac.ConditionalRoleManager + matcherMap sync.Map + logger log.Logger + detectors []detector.Detector + + enabled bool + autoSave bool + autoBuildRoleLinks bool + autoNotifyWatcher bool + autoNotifyDispatcher bool + acceptJsonRequest bool + gFunctionCache bool + + aiConfig AIConfig +} + +// EnforceContext is used as the first element of the parameter "rvals" in method "enforce". +type EnforceContext struct { + RType string + PType string + EType string + MType string +} + +func (e EnforceContext) GetCacheKey() string { + return "EnforceContext{" + e.RType + "-" + e.PType + "-" + e.EType + "-" + e.MType + "}" +} + +// NewEnforcer creates an enforcer via file or DB. +// +// File: +// +// e := casbin.NewEnforcer("path/to/basic_model.conf", "path/to/basic_policy.csv") +// +// MySQL DB: +// +// a := mysqladapter.NewDBAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/") +// e := casbin.NewEnforcer("path/to/basic_model.conf", a) +func NewEnforcer(params ...interface{}) (*Enforcer, error) { + e := &Enforcer{} + + // Bind the parameters up front so the accesses below are provably in range. + var param0, param1 interface{} + if len(params) > 0 { + param0 = params[0] + } + if len(params) > 1 { + param1 = params[1] + } + + switch len(params) { + case 2: + switch p0 := param0.(type) { + case string: + switch p1 := param1.(type) { + case string: + err := e.InitWithFile(p0, p1) + if err != nil { + return nil, err + } + default: + err := e.InitWithAdapter(p0, p1.(persist.Adapter)) + if err != nil { + return nil, err + } + } + default: + switch param1.(type) { + case string: + return nil, errors.New("invalid parameters for enforcer") + default: + err := e.InitWithModelAndAdapter(p0.(model.Model), param1.(persist.Adapter)) + if err != nil { + return nil, err + } + } + } + case 1: + switch p0 := param0.(type) { + case string: + err := e.InitWithFile(p0, "") + if err != nil { + return nil, err + } + default: + err := e.InitWithModelAndAdapter(p0.(model.Model), nil) + if err != nil { + return nil, err + } + } + case 0: + return e, nil + default: + return nil, errors.New("invalid parameters for enforcer") + } + + return e, nil +} + +// InitWithFile initializes an enforcer with a model file and a policy file. +func (e *Enforcer) InitWithFile(modelPath string, policyPath string) error { + a := fileadapter.NewAdapter(policyPath) + return e.InitWithAdapter(modelPath, a) +} + +// InitWithAdapter initializes an enforcer with a database adapter. +func (e *Enforcer) InitWithAdapter(modelPath string, adapter persist.Adapter) error { + m, err := model.NewModelFromFile(modelPath) + if err != nil { + return err + } + + err = e.InitWithModelAndAdapter(m, adapter) + if err != nil { + return err + } + + e.modelPath = modelPath + return nil +} + +// InitWithModelAndAdapter initializes an enforcer with a model and a database adapter. +func (e *Enforcer) InitWithModelAndAdapter(m model.Model, adapter persist.Adapter) error { + e.adapter = adapter + + e.model = m + e.model.PrintModel() + e.fm = model.LoadFunctionMap() + + e.initialize() + + // Do not initialize the full policy when using a filtered adapter + fa, ok := e.adapter.(persist.FilteredAdapter) + if e.adapter != nil && (!ok || ok && !fa.IsFiltered()) { + err := e.LoadPolicy() + if err != nil { + return err + } + } + + return nil +} + +func (e *Enforcer) initialize() { + e.rmMap = map[string]rbac.RoleManager{} + e.condRmMap = map[string]rbac.ConditionalRoleManager{} + e.eft = effector.NewDefaultEffector() + e.watcher = nil + e.matcherMap = sync.Map{} + + e.enabled = true + e.autoSave = true + e.autoBuildRoleLinks = true + e.autoNotifyWatcher = true + e.autoNotifyDispatcher = true + e.gFunctionCache = true + e.initRmMap() + + // Initialize detectors with default detector if not already set + if e.detectors == nil { + e.detectors = []detector.Detector{detector.NewDefaultDetector()} + } +} + +// LoadModel reloads the model from the model CONF file. +// Because the policy is attached to a model, so the policy is invalidated and needs to be reloaded by calling LoadPolicy(). +func (e *Enforcer) LoadModel() error { + var err error + e.model, err = model.NewModelFromFile(e.modelPath) + if err != nil { + return err + } + + e.model.PrintModel() + e.fm = model.LoadFunctionMap() + + e.initialize() + + return nil +} + +// GetModel gets the current model. +func (e *Enforcer) GetModel() model.Model { + return e.model +} + +// SetModel sets the current model. +func (e *Enforcer) SetModel(m model.Model) { + e.model = m + e.fm = model.LoadFunctionMap() + + e.initialize() +} + +// GetAdapter gets the current adapter. +func (e *Enforcer) GetAdapter() persist.Adapter { + return e.adapter +} + +// SetAdapter sets the current adapter. +func (e *Enforcer) SetAdapter(adapter persist.Adapter) { + e.adapter = adapter +} + +// SetWatcher sets the current watcher. +func (e *Enforcer) SetWatcher(watcher persist.Watcher) error { + e.watcher = watcher + if _, ok := e.watcher.(persist.WatcherEx); ok { + // The callback of WatcherEx has no generic implementation. + return nil + } else { + // In case the Watcher wants to use a customized callback function, call `SetUpdateCallback` after `SetWatcher`. + return watcher.SetUpdateCallback(func(string) { _ = e.LoadPolicy() }) + } +} + +// GetRoleManager gets the current role manager. +func (e *Enforcer) GetRoleManager() rbac.RoleManager { + if e.rmMap != nil && e.rmMap["g"] != nil { + return e.rmMap["g"] + } else if e.condRmMap != nil && e.condRmMap["g"] != nil { + return e.condRmMap["g"] + } else { + return nil + } +} + +// GetNamedRoleManager gets the role manager for the named policy. +func (e *Enforcer) GetNamedRoleManager(ptype string) rbac.RoleManager { + if e.rmMap != nil && e.rmMap[ptype] != nil { + return e.rmMap[ptype] + } else if e.condRmMap != nil && e.condRmMap[ptype] != nil { + return e.condRmMap[ptype] + } else { + return nil + } +} + +// SetRoleManager sets the current role manager. +func (e *Enforcer) SetRoleManager(rm rbac.RoleManager) { + e.invalidateMatcherMap() + e.rmMap["g"] = rm +} + +// SetNamedRoleManager sets the role manager for the named policy. +func (e *Enforcer) SetNamedRoleManager(ptype string, rm rbac.RoleManager) { + e.invalidateMatcherMap() + e.rmMap[ptype] = rm +} + +// SetEffector sets the current effector. +func (e *Enforcer) SetEffector(eft effector.Effector) { + e.eft = eft +} + +// SetLogger sets the logger for the enforcer. +func (e *Enforcer) SetLogger(logger log.Logger) { + e.logger = logger +} + +// SetDetector sets a single detector for the enforcer. +func (e *Enforcer) SetDetector(d detector.Detector) { + e.detectors = []detector.Detector{d} +} + +// SetDetectors sets multiple detectors for the enforcer. +func (e *Enforcer) SetDetectors(detectors []detector.Detector) { + e.detectors = detectors +} + +// RunDetections runs all detectors on all role managers. +// Returns the first error encountered, or nil if all checks pass. +// Silently skips role managers that don't support the required iteration methods. +func (e *Enforcer) RunDetections() error { + if len(e.detectors) == 0 { + return nil + } + + // Run detectors on all role managers + for _, rm := range e.rmMap { + for _, d := range e.detectors { + err := d.Check(rm) + // Skip if the role manager doesn't support the required iteration or is not initialized + if err != nil && (strings.Contains(err.Error(), "does not support Range iteration") || + strings.Contains(err.Error(), "not properly initialized")) { + continue + } + if err != nil { + return err + } + } + } + + // Run detectors on all conditional role managers + for _, crm := range e.condRmMap { + for _, d := range e.detectors { + err := d.Check(crm) + // Skip if the role manager doesn't support the required iteration or is not initialized + if err != nil && (strings.Contains(err.Error(), "does not support Range iteration") || + strings.Contains(err.Error(), "not properly initialized")) { + continue + } + if err != nil { + return err + } + } + } + + return nil +} + +// ClearPolicy clears all policy. +func (e *Enforcer) ClearPolicy() { + e.invalidateMatcherMap() + + if e.dispatcher != nil && e.autoNotifyDispatcher { + _ = e.dispatcher.ClearPolicy() + return + } + e.model.ClearPolicy() +} + +// LoadPolicy reloads the policy from file/database. +func (e *Enforcer) LoadPolicy() error { + logEntry := e.onLogBeforeEventInLoadPolicy() + + newModel, err := e.loadPolicyFromAdapter(e.model) + if err != nil { + e.onLogAfterEventWithError(logEntry, err) + return err + } + err = e.applyModifiedModel(newModel) + if err != nil { + e.onLogAfterEventWithError(logEntry, err) + return err + } + + e.onLogAfterEventInLoadPolicy(logEntry, newModel) + + // Run detectors after all policy rules are loaded + err = e.RunDetections() + if err != nil { + return err + } + + return nil +} + +func (e *Enforcer) loadPolicyFromAdapter(baseModel model.Model) (model.Model, error) { + newModel := baseModel.Copy() + newModel.ClearPolicy() + + if err := e.adapter.LoadPolicy(newModel); err != nil && err.Error() != "invalid file path, file path cannot be empty" { + return nil, err + } + + if err := newModel.SortPoliciesBySubjectHierarchy(); err != nil { + return nil, err + } + + if err := newModel.SortPoliciesByPriority(); err != nil { + return nil, err + } + + return newModel, nil +} + +func (e *Enforcer) applyModifiedModel(newModel model.Model) error { + var err error + needToRebuild := false + defer func() { + if err != nil { + if e.autoBuildRoleLinks && needToRebuild { + _ = e.BuildRoleLinks() + } + } + }() + + if e.autoBuildRoleLinks { + needToRebuild = true + + if err := e.rebuildRoleLinks(newModel); err != nil { + return err + } + + if err := e.rebuildConditionalRoleLinks(newModel); err != nil { + return err + } + } + + e.model = newModel + e.invalidateMatcherMap() + return nil +} + +func (e *Enforcer) rebuildRoleLinks(newModel model.Model) error { + if len(e.rmMap) != 0 { + for _, rm := range e.rmMap { + err := rm.Clear() + if err != nil { + return err + } + } + + err := newModel.BuildRoleLinks(e.rmMap) + if err != nil { + return err + } + } + + return nil +} + +func (e *Enforcer) rebuildConditionalRoleLinks(newModel model.Model) error { + if len(e.condRmMap) != 0 { + for _, crm := range e.condRmMap { + err := crm.Clear() + if err != nil { + return err + } + } + + err := newModel.BuildConditionalRoleLinks(e.condRmMap) + if err != nil { + return err + } + } + return nil +} + +func (e *Enforcer) loadFilteredPolicy(filter interface{}) error { + e.invalidateMatcherMap() + + var filteredAdapter persist.FilteredAdapter + + // Attempt to cast the Adapter as a FilteredAdapter + switch adapter := e.adapter.(type) { + case persist.FilteredAdapter: + filteredAdapter = adapter + default: + return errors.New("filtered policies are not supported by this adapter") + } + if err := filteredAdapter.LoadFilteredPolicy(e.model, filter); err != nil && err.Error() != "invalid file path, file path cannot be empty" { + return err + } + + if err := e.model.SortPoliciesBySubjectHierarchy(); err != nil { + return err + } + + if err := e.model.SortPoliciesByPriority(); err != nil { + return err + } + + e.initRmMap() + e.model.PrintPolicy() + if e.autoBuildRoleLinks { + err := e.BuildRoleLinks() + if err != nil { + return err + } + } + return nil +} + +// LoadFilteredPolicy reloads a filtered policy from file/database. +func (e *Enforcer) LoadFilteredPolicy(filter interface{}) error { + e.model.ClearPolicy() + + return e.loadFilteredPolicy(filter) +} + +// LoadIncrementalFilteredPolicy append a filtered policy from file/database. +func (e *Enforcer) LoadIncrementalFilteredPolicy(filter interface{}) error { + return e.loadFilteredPolicy(filter) +} + +// IsFiltered returns true if the loaded policy has been filtered. +func (e *Enforcer) IsFiltered() bool { + filteredAdapter, ok := e.adapter.(persist.FilteredAdapter) + if !ok { + return false + } + return filteredAdapter.IsFiltered() +} + +// SavePolicy saves the current policy (usually after changed with Casbin API) back to file/database. +func (e *Enforcer) SavePolicy() error { + logEntry := e.onLogBeforeEventInSavePolicy() + + if e.IsFiltered() { + err := errors.New("cannot save a filtered policy") + e.onLogAfterEventWithError(logEntry, err) + return err + } + if err := e.adapter.SavePolicy(e.model); err != nil { + e.onLogAfterEventWithError(logEntry, err) + return err + } + + e.onLogAfterEventInSavePolicy(logEntry) + + if e.watcher != nil { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForSavePolicy(e.model) + } else { + err = e.watcher.Update() + } + return err + } + return nil +} + +// getDomainTokens extracts domain token names from request and policy definitions. +// Returns empty strings if tokens cannot be found. +func (e *Enforcer) getDomainTokens() (rDomainToken, pDomainToken string) { + if rAssertion, ok := e.model["r"]["r"]; ok && len(rAssertion.Tokens) > 1 { + rDomainToken = rAssertion.Tokens[1] + } + if pAssertion, ok := e.model["p"]["p"]; ok && len(pAssertion.Tokens) > 1 { + pDomainToken = pAssertion.Tokens[1] + } + return rDomainToken, pDomainToken +} + +// registerDomainMatchingFunc registers domain matching function if the matcher uses keyMatch for domains. +func (e *Enforcer) registerDomainMatchingFunc(ptype string) { + // Dynamically detect the domain token name from the model definition. + // In RBAC with domains, the domain is typically the second parameter (index 1) + // in both request and policy definitions (e.g., r = sub, dom, obj, act). + // We extract the actual token names to support arbitrary domain parameter names. + rDomainToken, pDomainToken := e.getDomainTokens() + if rDomainToken == "" || pDomainToken == "" { + return + } + + matchFun := fmt.Sprintf("keyMatch(%s, %s)", rDomainToken, pDomainToken) + if strings.Contains(e.model["m"]["m"].Value, matchFun) { + e.AddNamedDomainMatchingFunc(ptype, "g", util.KeyMatch) + } +} + +func (e *Enforcer) initRmMap() { + for ptype, assertion := range e.model["g"] { + if rm, ok := e.rmMap[ptype]; ok { + _ = rm.Clear() + continue + } + if len(assertion.Tokens) <= 2 && len(assertion.ParamsTokens) == 0 { + assertion.RM = defaultrolemanager.NewRoleManagerImpl(10) + e.rmMap[ptype] = assertion.RM + } + if len(assertion.Tokens) <= 2 && len(assertion.ParamsTokens) != 0 { + assertion.CondRM = defaultrolemanager.NewConditionalRoleManager(10) + e.condRmMap[ptype] = assertion.CondRM + } + if len(assertion.Tokens) > 2 { + if len(assertion.ParamsTokens) == 0 { + assertion.RM = defaultrolemanager.NewRoleManager(10) + e.rmMap[ptype] = assertion.RM + } else { + assertion.CondRM = defaultrolemanager.NewConditionalDomainManager(10) + e.condRmMap[ptype] = assertion.CondRM + } + e.registerDomainMatchingFunc(ptype) + } + } +} + +// EnableEnforce changes the enforcing state of Casbin, when Casbin is disabled, all access will be allowed by the Enforce() function. +func (e *Enforcer) EnableEnforce(enable bool) { + e.enabled = enable +} + +// EnableAutoNotifyWatcher controls whether to save a policy rule automatically notify the Watcher when it is added or removed. +func (e *Enforcer) EnableAutoNotifyWatcher(enable bool) { + e.autoNotifyWatcher = enable +} + +// EnableAutoNotifyDispatcher controls whether to save a policy rule automatically notify the Dispatcher when it is added or removed. +func (e *Enforcer) EnableAutoNotifyDispatcher(enable bool) { + e.autoNotifyDispatcher = enable +} + +// EnableAutoSave controls whether to save a policy rule automatically to the adapter when it is added or removed. +func (e *Enforcer) EnableAutoSave(autoSave bool) { + e.autoSave = autoSave +} + +// EnableAutoBuildRoleLinks controls whether to rebuild the role inheritance relations when a role is added or deleted. +func (e *Enforcer) EnableAutoBuildRoleLinks(autoBuildRoleLinks bool) { + e.autoBuildRoleLinks = autoBuildRoleLinks +} + +// EnableAcceptJsonRequest controls whether to accept json as a request parameter. +func (e *Enforcer) EnableAcceptJsonRequest(acceptJsonRequest bool) { + e.acceptJsonRequest = acceptJsonRequest +} + +// EnableGFunctionCache controls whether to cache g() function results. +// Disable this when inputs are high-cardinality (e.g. UUIDs or dynamic paths) +// to prevent unbounded memory growth. +func (e *Enforcer) EnableGFunctionCache(enabled bool) { + e.gFunctionCache = enabled + e.invalidateMatcherMap() +} + +// BuildRoleLinks manually rebuild the role inheritance relations. +func (e *Enforcer) BuildRoleLinks() error { + e.invalidateMatcherMap() + if e.rmMap == nil { + return errors.New("rmMap is nil") + } + for _, rm := range e.rmMap { + err := rm.Clear() + if err != nil { + return err + } + } + + return e.model.BuildRoleLinks(e.rmMap) +} + +// BuildIncrementalRoleLinks provides incremental build the role inheritance relations. +func (e *Enforcer) BuildIncrementalRoleLinks(op model.PolicyOp, ptype string, rules [][]string) error { + e.invalidateMatcherMap() + return e.model.BuildIncrementalRoleLinks(e.rmMap, op, "g", ptype, rules) +} + +// BuildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations with conditions. +func (e *Enforcer) BuildIncrementalConditionalRoleLinks(op model.PolicyOp, ptype string, rules [][]string) error { + e.invalidateMatcherMap() + return e.model.BuildIncrementalConditionalRoleLinks(e.condRmMap, op, "g", ptype, rules) +} + +// NewEnforceContext Create a default structure based on the suffix. +func NewEnforceContext(suffix string) EnforceContext { + return EnforceContext{ + RType: "r" + suffix, + PType: "p" + suffix, + EType: "e" + suffix, + MType: "m" + suffix, + } +} + +func (e *Enforcer) invalidateMatcherMap() { + e.matcherMap = sync.Map{} +} + +// enforce use a custom matcher to decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (matcher, sub, obj, act), use model matcher by default when matcher is "". +func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interface{}) (ok bool, err error) { //nolint:funlen,cyclop,gocyclo // TODO: reduce function complexity + logEntry := e.onLogBeforeEventInEnforce(rvals) + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v\n%s", r, debug.Stack()) + if e.logger != nil && logEntry != nil { + logEntry.Error = err + } + } + e.onLogAfterEventInEnforce(logEntry, ok) + }() + + if !e.enabled { + return true, nil + } + + functions := e.fm.GetFunctions() + if _, ok := e.model["g"]; ok { + for key, ast := range e.model["g"] { + // g must be a normal role definition (ast.RM != nil) + // or a conditional role definition (ast.CondRM != nil) + // ast.RM and ast.CondRM shouldn't be nil at the same time + if ast.RM != nil { + functions[key] = util.GenerateGFunction(ast.RM, e.gFunctionCache) + } + if ast.CondRM != nil { + functions[key] = util.GenerateConditionalGFunction(ast.CondRM) + } + } + } + + var ( + rType = "r" + pType = "p" + eType = "e" + mType = "m" + ) + if len(rvals) != 0 { + switch rvals[0].(type) { + case EnforceContext: + enforceContext := rvals[0].(EnforceContext) + rType = enforceContext.RType + pType = enforceContext.PType + eType = enforceContext.EType + mType = enforceContext.MType + rvals = rvals[1:] + default: + break + } + } + + var expString string + if matcher == "" { + expString = e.model["m"][mType].Value + } else { + // For custom matchers provided at runtime, escape backslashes in string literals + expString = util.EscapeStringLiterals(util.RemoveComments(util.EscapeAssertion(matcher))) + } + + rTokens := make(map[string]int, len(e.model["r"][rType].Tokens)) + for i, token := range e.model["r"][rType].Tokens { + rTokens[token] = i + } + pTokens := make(map[string]int, len(e.model["p"][pType].Tokens)) + for i, token := range e.model["p"][pType].Tokens { + pTokens[token] = i + } + + if e.acceptJsonRequest { + // try to parse all request values from json to map[string]interface{} + for i, rval := range rvals { + switch rval := rval.(type) { + case string: + // Only attempt JSON parsing for strings that look like JSON objects or arrays + if len(rval) > 0 && (rval[0] == '{' || rval[0] == '[') { + var mapValue map[string]interface{} + mapValue, err = util.JsonToMap(rval) + if err != nil { + // Return a clear error when JSON-like string fails to parse + return false, fmt.Errorf("failed to parse JSON parameter at index %d: %w", i, err) + } + rvals[i] = mapValue + } + } + } + } + + parameters := enforceParameters{ + rTokens: rTokens, + rVals: rvals, + + pTokens: pTokens, + } + + hasEval := util.HasEval(expString) + if hasEval { + functions["eval"] = generateEvalFunction(functions, ¶meters) + } + var expression *govaluate.EvaluableExpression + expression, err = e.getAndStoreMatcherExpression(hasEval, expString, functions) + if err != nil { + return false, err + } + + if len(e.model["r"][rType].Tokens) != len(rvals) { + return false, fmt.Errorf( + "invalid request size: expected %d, got %d, rvals: %v", + len(e.model["r"][rType].Tokens), + len(rvals), + rvals) + } + + var policyEffects []effector.Effect + var matcherResults []float64 + + var effect effector.Effect + var explainIndex int + + if policyLen := len(e.model["p"][pType].Policy); policyLen != 0 && strings.Contains(expString, pType+"_") { //nolint:nestif // TODO: reduce function complexity + policyEffects = make([]effector.Effect, policyLen) + matcherResults = make([]float64, policyLen) + + for policyIndex, pvals := range e.model["p"][pType].Policy { + // log.LogPrint("Policy Rule: ", pvals) + if len(e.model["p"][pType].Tokens) != len(pvals) { + return false, fmt.Errorf( + "invalid policy size: expected %d, got %d, pvals: %v", + len(e.model["p"][pType].Tokens), + len(pvals), + pvals) + } + + parameters.pVals = pvals + + result, err := expression.Eval(parameters) + // log.LogPrint("Result: ", result) + if err != nil { + return false, err + } + + // set to no-match at first + matcherResults[policyIndex] = 0 + switch result := result.(type) { + case bool: + if result { + matcherResults[policyIndex] = 1 + } + case float64: + if result != 0 { + matcherResults[policyIndex] = 1 + } + default: + return false, errors.New("matcher result should be bool, int or float") + } + + if j, ok := parameters.pTokens[pType+"_eft"]; ok { + eft := parameters.pVals[j] + switch eft { + case "allow": + policyEffects[policyIndex] = effector.Allow + case "deny": + policyEffects[policyIndex] = effector.Deny + default: + policyEffects[policyIndex] = effector.Indeterminate + } + } else { + policyEffects[policyIndex] = effector.Allow + } + + // if e.model["e"]["e"].Value == "priority(p_eft) || deny" { + // break + // } + + effect, explainIndex, err = e.eft.MergeEffects(e.model["e"][eType].Value, policyEffects, matcherResults, policyIndex, policyLen) + if err != nil { + return false, err + } + if effect != effector.Indeterminate { + break + } + } + } else { + if hasEval && len(e.model["p"][pType].Policy) == 0 { + return false, errors.New("please make sure rule exists in policy when using eval() in matcher") + } + + policyEffects = make([]effector.Effect, 1) + matcherResults = make([]float64, 1) + matcherResults[0] = 1 + + parameters.pVals = make([]string, len(parameters.pTokens)) + + result, err := expression.Eval(parameters) + if err != nil { + return false, err + } + + if result.(bool) { + policyEffects[0] = effector.Allow + } else { + policyEffects[0] = effector.Indeterminate + } + + effect, explainIndex, err = e.eft.MergeEffects(e.model["e"][eType].Value, policyEffects, matcherResults, 0, 1) + if err != nil { + return false, err + } + } + + if explains != nil { + if explainIndex != -1 && len(e.model["p"][pType].Policy) > explainIndex { + *explains = e.model["p"][pType].Policy[explainIndex] + } + } + + // effect -> result + result := effect == effector.Allow + + return result, nil +} + +func (e *Enforcer) getAndStoreMatcherExpression(hasEval bool, expString string, functions map[string]govaluate.ExpressionFunction) (*govaluate.EvaluableExpression, error) { + var expression *govaluate.EvaluableExpression + var err error + cachedExpression, isPresent := e.matcherMap.Load(expString) + + if !hasEval && isPresent { + expression = cachedExpression.(*govaluate.EvaluableExpression) + } else { + expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions) + if err != nil { + return nil, err + } + e.matcherMap.Store(expString, expression) + } + return expression, nil +} + +// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act). +func (e *Enforcer) Enforce(rvals ...interface{}) (bool, error) { + return e.enforce("", nil, rvals...) +} + +// EnforceWithMatcher use a custom matcher to decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (matcher, sub, obj, act), use model matcher by default when matcher is "". +func (e *Enforcer) EnforceWithMatcher(matcher string, rvals ...interface{}) (bool, error) { + return e.enforce(matcher, nil, rvals...) +} + +// EnforceEx explain enforcement by informing matched rules. +func (e *Enforcer) EnforceEx(rvals ...interface{}) (bool, []string, error) { + explain := []string{} + result, err := e.enforce("", &explain, rvals...) + return result, explain, err +} + +// EnforceExWithMatcher use a custom matcher and explain enforcement by informing matched rules. +func (e *Enforcer) EnforceExWithMatcher(matcher string, rvals ...interface{}) (bool, []string, error) { + explain := []string{} + result, err := e.enforce(matcher, &explain, rvals...) + return result, explain, err +} + +// BatchEnforce enforce in batches. +func (e *Enforcer) BatchEnforce(requests [][]interface{}) ([]bool, error) { + var results []bool + for _, request := range requests { + result, err := e.enforce("", nil, request...) + if err != nil { + return results, err + } + results = append(results, result) + } + return results, nil +} + +// BatchEnforceWithMatcher enforce with matcher in batches. +func (e *Enforcer) BatchEnforceWithMatcher(matcher string, requests [][]interface{}) ([]bool, error) { + var results []bool + for _, request := range requests { + result, err := e.enforce(matcher, nil, request...) + if err != nil { + return results, err + } + results = append(results, result) + } + return results, nil +} + +// AddNamedMatchingFunc add MatchingFunc by ptype RoleManager. +func (e *Enforcer) AddNamedMatchingFunc(ptype, name string, fn rbac.MatchingFunc) bool { + if rm, ok := e.rmMap[ptype]; ok { + rm.AddMatchingFunc(name, fn) + return true + } + return false +} + +// AddNamedDomainMatchingFunc add MatchingFunc by ptype to RoleManager. +func (e *Enforcer) AddNamedDomainMatchingFunc(ptype, name string, fn rbac.MatchingFunc) bool { + if rm, ok := e.rmMap[ptype]; ok { + rm.AddDomainMatchingFunc(name, fn) + return true + } + if condRm, ok := e.condRmMap[ptype]; ok { + condRm.AddDomainMatchingFunc(name, fn) + return true + } + return false +} + +// AddNamedLinkConditionFunc Add condition function fn for Link userName->roleName, +// when fn returns true, Link is valid, otherwise invalid. +func (e *Enforcer) AddNamedLinkConditionFunc(ptype, user, role string, fn rbac.LinkConditionFunc) bool { + if rm, ok := e.condRmMap[ptype]; ok { + rm.AddLinkConditionFunc(user, role, fn) + return true + } + return false +} + +// AddNamedDomainLinkConditionFunc Add condition function fn for Link userName-> {roleName, domain}, +// when fn returns true, Link is valid, otherwise invalid. +func (e *Enforcer) AddNamedDomainLinkConditionFunc(ptype, user, role string, domain string, fn rbac.LinkConditionFunc) bool { + if rm, ok := e.condRmMap[ptype]; ok { + rm.AddDomainLinkConditionFunc(user, role, domain, fn) + return true + } + return false +} + +// SetNamedLinkConditionFuncParams Sets the parameters of the condition function fn for Link userName->roleName. +func (e *Enforcer) SetNamedLinkConditionFuncParams(ptype, user, role string, params ...string) bool { + if rm, ok := e.condRmMap[ptype]; ok { + rm.SetLinkConditionFuncParams(user, role, params...) + return true + } + return false +} + +// SetNamedDomainLinkConditionFuncParams Sets the parameters of the condition function fn +// for Link userName->{roleName, domain}. +func (e *Enforcer) SetNamedDomainLinkConditionFuncParams(ptype, user, role, domain string, params ...string) bool { + if rm, ok := e.condRmMap[ptype]; ok { + rm.SetDomainLinkConditionFuncParams(user, role, domain, params...) + return true + } + return false +} + +// assumes bounds have already been checked. +type enforceParameters struct { + rTokens map[string]int + rVals []interface{} + + pTokens map[string]int + pVals []string +} + +// implements govaluate.Parameters. +func (p enforceParameters) Get(name string) (interface{}, error) { + if name == "" { + return nil, nil + } + + switch name[0] { + case 'p': + i, ok := p.pTokens[name] + if !ok { + return nil, errors.New("No parameter '" + name + "' found.") + } + return p.pVals[i], nil + case 'r': + i, ok := p.rTokens[name] + if !ok { + return nil, errors.New("No parameter '" + name + "' found.") + } + return p.rVals[i], nil + default: + return nil, errors.New("No parameter '" + name + "' found.") + } +} + +func generateEvalFunction(functions map[string]govaluate.ExpressionFunction, parameters *enforceParameters) govaluate.ExpressionFunction { + return func(args ...interface{}) (interface{}, error) { + if len(args) != 1 { + return nil, fmt.Errorf("function eval(subrule string) expected %d arguments, but got %d", 1, len(args)) + } + + expression, ok := args[0].(string) + if !ok { + return nil, errors.New("argument of eval(subrule string) must be a string") + } + expression = util.EscapeAssertion(expression) + expr, err := govaluate.NewEvaluableExpressionWithFunctions(expression, functions) + if err != nil { + return nil, fmt.Errorf("error while parsing eval parameter: %s, %s", expression, err.Error()) + } + return expr.Eval(parameters) + } +} diff --git a/enforcer_backslash_test.go b/enforcer_backslash_test.go new file mode 100644 index 0000000..a62498e --- /dev/null +++ b/enforcer_backslash_test.go @@ -0,0 +1,180 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" + + "github.com/casbin/casbin/v3/model" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" +) + +// TestBackslashHandlingConsistency tests that backslashes in string literals +// within matcher expressions are handled consistently with CSV-parsed values. +// This addresses the issue where govaluate interprets escape sequences in +// string literals, but CSV parsing treats backslashes as literal characters. +func TestBackslashHandlingConsistency(t *testing.T) { + // Test case 1: Literal string in matcher should match CSV-parsed request + t.Run("LiteralInMatcher", func(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("e", "e", "some(where (p.eft == allow))") + // User writes '\1\2' in matcher - should be treated as literal backslashes + m.AddDef("m", "m", "regexMatch('\\1\\2', p.obj)") + + e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv")) + if err != nil { + t.Fatal(err) + } + + // Add a policy with a regex pattern containing backslashes + // CSV format: "\\[0-9]+\\" means literal string with 4 backslashes + _, err = e.AddPolicy("filename", "\\\\[0-9]+\\\\", "read") + if err != nil { + t.Fatal(err) + } + + // This should match because '\1\2' after escaping becomes \1\2, + // and the pattern \\[0-9]+\\ matches strings like \1\ (which is a substring of \1\2) + result, err := e.Enforce("filename", "dummy", "read") + if err != nil { + t.Fatal(err) + } + + if !result { + t.Errorf("Expected true, got false - literal '\\1\\2' should match after escape processing") + } + }) + + // Test case 2: Request parameter should match policy with same backslash content + t.Run("RequestParameterVsPolicy", func(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("e", "e", "some(where (p.eft == allow))") + m.AddDef("m", "m", "regexMatch(r.obj, p.obj)") + + e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv")) + if err != nil { + t.Fatal(err) + } + + // Add policy with regex pattern + _, err = e.AddPolicy("filename", "\\\\[0-9]+\\\\", "read") + if err != nil { + t.Fatal(err) + } + + // Request with backslashes - simulating CSV input "\1\2" which becomes \1\2 + result, err := e.Enforce("filename", "\\1\\2", "read") + if err != nil { + t.Fatal(err) + } + + if !result { + t.Errorf("Expected true, got false - request \\1\\2 should match regex pattern") + } + }) + + // Test case 3: Both approaches should give the same result + t.Run("ConsistencyBetweenLiteralAndParameter", func(t *testing.T) { + // Create two enforcers with different matchers + m1 := model.NewModel() + m1.AddDef("r", "r", "sub, obj, act") + m1.AddDef("p", "p", "sub, obj, act") + m1.AddDef("e", "e", "some(where (p.eft == allow))") + m1.AddDef("m", "m", "regexMatch('\\1\\2', p.obj)") + + m2 := model.NewModel() + m2.AddDef("r", "r", "sub, obj, act") + m2.AddDef("p", "p", "sub, obj, act") + m2.AddDef("e", "e", "some(where (p.eft == allow))") + m2.AddDef("m", "m", "regexMatch(r.obj, p.obj)") + + e1, err := NewEnforcer(m1, fileadapter.NewAdapter("examples/basic_policy.csv")) + if err != nil { + t.Fatal(err) + } + e2, err := NewEnforcer(m2, fileadapter.NewAdapter("examples/basic_policy.csv")) + if err != nil { + t.Fatal(err) + } + + // Add same policy to both + pattern := "\\\\[0-9]+\\\\" + _, err = e1.AddPolicy("filename", pattern, "read") + if err != nil { + t.Fatal(err) + } + _, err = e2.AddPolicy("filename", pattern, "read") + if err != nil { + t.Fatal(err) + } + + // Test with the same request + result1, err := e1.Enforce("filename", "dummy", "read") + if err != nil { + t.Fatal(err) + } + + result2, err := e2.Enforce("filename", "\\1\\2", "read") + if err != nil { + t.Fatal(err) + } + + if result1 != result2 { + t.Errorf("Inconsistent results: literal in matcher gave %v, parameter gave %v", result1, result2) + } + }) + + // Test case 4: Simple equality check with backslashes + t.Run("SimpleEqualityWithBackslashes", func(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("e", "e", "some(where (p.eft == allow))") + // In Go source, '\test' (one backslash in the actual string) represents + // what would be typed in a web form. After escape processing, it will match + // the CSV-parsed value "\test" (one backslash). + // Note: In Go source, we write "r.obj == '\\test'" which is a Go string + // containing the text: r.obj == '\test' (with ONE backslash in the string content) + m.AddDef("m", "m", "r.obj == '\\test' && p.sub == r.sub") + + e, err := NewEnforcer(m, fileadapter.NewAdapter("examples/basic_policy.csv")) + if err != nil { + t.Fatal(err) + } + + _, err = e.AddPolicy("alice", "any", "read") + if err != nil { + t.Fatal(err) + } + + // Request with literal backslash from CSV would be "\test" + // In Go source, we write "\\test" which represents the string \test (one backslash) + result, err := e.Enforce("alice", "\\test", "read") + if err != nil { + t.Fatal(err) + } + + if !result { + t.Errorf("Expected true - literal '\\test' should equal request parameter \\test") + } + }) +} diff --git a/enforcer_cached.go b/enforcer_cached.go new file mode 100644 index 0000000..a9b4be4 --- /dev/null +++ b/enforcer_cached.go @@ -0,0 +1,188 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/casbin/casbin/v3/persist/cache" +) + +// CachedEnforcer wraps Enforcer and provides decision cache. +type CachedEnforcer struct { + *Enforcer + expireTime time.Duration + cache cache.Cache + enableCache int32 + locker *sync.RWMutex +} + +type CacheableParam interface { + GetCacheKey() string +} + +// NewCachedEnforcer creates a cached enforcer via file or DB. +func NewCachedEnforcer(params ...interface{}) (*CachedEnforcer, error) { + e := &CachedEnforcer{} + var err error + e.Enforcer, err = NewEnforcer(params...) + if err != nil { + return nil, err + } + + e.enableCache = 1 + e.cache, _ = cache.NewDefaultCache() + e.locker = new(sync.RWMutex) + return e, nil +} + +// EnableCache determines whether to enable cache on Enforce(). When enableCache is enabled, cached result (true | false) will be returned for previous decisions. +func (e *CachedEnforcer) EnableCache(enableCache bool) { + var enabled int32 + if enableCache { + enabled = 1 + } + atomic.StoreInt32(&e.enableCache, enabled) +} + +// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act). +// if rvals is not string , ignore the cache. +func (e *CachedEnforcer) Enforce(rvals ...interface{}) (bool, error) { + if atomic.LoadInt32(&e.enableCache) == 0 { + return e.Enforcer.Enforce(rvals...) + } + + key, ok := e.getKey(rvals...) + if !ok { + return e.Enforcer.Enforce(rvals...) + } + + if res, err := e.getCachedResult(key); err == nil { + return res, nil + } else if err != cache.ErrNoSuchKey { + return res, err + } + + res, err := e.Enforcer.Enforce(rvals...) + if err != nil { + return false, err + } + + err = e.setCachedResult(key, res, e.expireTime) + return res, err +} + +func (e *CachedEnforcer) LoadPolicy() error { + if atomic.LoadInt32(&e.enableCache) != 0 { + if err := e.cache.Clear(); err != nil { + return err + } + } + return e.Enforcer.LoadPolicy() +} + +func (e *CachedEnforcer) RemovePolicy(params ...interface{}) (bool, error) { + if atomic.LoadInt32(&e.enableCache) != 0 { + key, ok := e.getKey(params...) + if ok { + if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey { + return false, err + } + } + } + return e.Enforcer.RemovePolicy(params...) +} + +func (e *CachedEnforcer) RemovePolicies(rules [][]string) (bool, error) { + if len(rules) != 0 { + if atomic.LoadInt32(&e.enableCache) != 0 { + for _, rule := range rules { + irule := make([]interface{}, len(rule)) + for i, param := range rule { + irule[i] = param + } + key, _ := e.getKey(irule...) + if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey { + return false, err + } + } + } + } + return e.Enforcer.RemovePolicies(rules) +} + +func (e *CachedEnforcer) getCachedResult(key string) (res bool, err error) { + e.locker.Lock() + defer e.locker.Unlock() + return e.cache.Get(key) +} + +func (e *CachedEnforcer) SetExpireTime(expireTime time.Duration) { + e.expireTime = expireTime +} + +func (e *CachedEnforcer) SetCache(c cache.Cache) { + e.cache = c +} + +func (e *CachedEnforcer) setCachedResult(key string, res bool, extra ...interface{}) error { + e.locker.Lock() + defer e.locker.Unlock() + return e.cache.Set(key, res, extra...) +} + +func (e *CachedEnforcer) getKey(params ...interface{}) (string, bool) { + return GetCacheKey(params...) +} + +// InvalidateCache deletes all the existing cached decisions. +func (e *CachedEnforcer) InvalidateCache() error { + e.locker.Lock() + defer e.locker.Unlock() + return e.cache.Clear() +} + +func GetCacheKey(params ...interface{}) (string, bool) { + key := strings.Builder{} + for _, param := range params { + switch typedParam := param.(type) { + case string: + key.WriteString(typedParam) + case CacheableParam: + key.WriteString(typedParam.GetCacheKey()) + default: + return "", false + } + key.WriteString("$$") + } + return key.String(), true +} + +// ClearPolicy clears all policy. +func (e *CachedEnforcer) ClearPolicy() { + if atomic.LoadInt32(&e.enableCache) != 0 { + if err := e.cache.Clear(); err != nil { + // Logger has been removed - error is ignored + return + } + } + e.Enforcer.ClearPolicy() +} diff --git a/enforcer_cached_b_test.go b/enforcer_cached_b_test.go new file mode 100644 index 0000000..2af891f --- /dev/null +++ b/enforcer_cached_b_test.go @@ -0,0 +1,224 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + "testing" +) + +func BenchmarkCachedRaw(b *testing.B) { + for i := 0; i < b.N; i++ { + rawEnforce("alice", "data1", "read") + } +} + +func BenchmarkCachedBasicModel(b *testing.B) { + e, _ := NewCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkCachedRBACModel(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data2", "read") + } +} + +func BenchmarkCachedRBACModelSmall(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_model.conf") + // 100 roles, 10 resources. + for i := 0; i < 100; i++ { + _, err := e.AddPolicy(fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read") + if err != nil { + b.Fatal(err) + } + } + // 1000 users. + for i := 0; i < 1000; i++ { + _, err := e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)) + if err != nil { + b.Fatal(err) + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("user501", "data9", "read") + } +} + +func BenchmarkCachedRBACModelMedium(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_model.conf") + // 1000 roles, 100 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 1000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 10000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("user5001", "data150", "read") + } +} + +func BenchmarkCachedRBACModelLarge(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 100000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 100000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("user50001", "data1500", "read") + } +} + +func BenchmarkCachedRBACModelWithResourceRoles(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_with_resource_roles_model.conf", "examples/rbac_with_resource_roles_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkCachedRBACModelWithDomains(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "domain1", "data1", "read") + } +} + +func BenchmarkCachedABACModel(b *testing.B) { + e, _ := NewCachedEnforcer("examples/abac_model.conf") + data1 := newTestResource("data1", "alice") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", data1, "read") + } +} + +func BenchmarkCachedKeyMatchModel(b *testing.B) { + e, _ := NewCachedEnforcer("examples/keymatch_model.conf", "examples/keymatch_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "/alice_data/resource1", "GET") + } +} + +func BenchmarkCachedRBACModelWithDeny(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_with_deny_model.conf", "examples/rbac_with_deny_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkCachedPriorityModel(b *testing.B) { + e, _ := NewCachedEnforcer("examples/priority_model.conf", "examples/priority_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkCachedWithEnforceContext(b *testing.B) { + e, _ := NewCachedEnforcer("examples/priority_model_enforce_context.conf", "examples/priority_policy_enforce_context.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce(EnforceContext{RType: "r2", PType: "p", EType: "e", MType: "m2"}, "alice", "data1") + } +} + +func BenchmarkCachedRBACModelMediumParallel(b *testing.B) { + e, _ := NewCachedEnforcer("examples/rbac_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 100000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 100000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, _ = e.Enforce("user5001", "data150", "read") + } + }) +} diff --git a/enforcer_cached_gfunction_test.go b/enforcer_cached_gfunction_test.go new file mode 100644 index 0000000..73e2f0c --- /dev/null +++ b/enforcer_cached_gfunction_test.go @@ -0,0 +1,204 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import "testing" + +// TestCachedGFunctionAfterAddingGroupingPolicy tests that adding new grouping policies +// at runtime correctly updates permission evaluation without requiring a restart or manual cache clearing. +// This is a regression test for the issue where GenerateGFunction's memoization cache +// caused stale permission results after adding new grouping policies. +func TestCachedGFunctionAfterAddingGroupingPolicy(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Initial state: bob should not have access to data2 (read) + // bob has no roles initially + ok, err := e.Enforce("bob", "data2", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if ok { + t.Error("bob should not have read access to data2 initially") + } + + // Add a new grouping policy: bob becomes data2_admin + // data2_admin has read and write access to data2 + _, err = e.AddGroupingPolicy("bob", "data2_admin") + if err != nil { + t.Fatalf("Failed to add grouping policy: %v", err) + } + + // Now bob should have read access to data2 through the data2_admin role + // This should work immediately without needing to clear cache or restart + ok, err = e.Enforce("bob", "data2", "read") + if err != nil { + t.Fatalf("Enforce failed after adding grouping policy: %v", err) + } + if !ok { + t.Error("bob should have read access to data2 after being added to data2_admin role") + } + + // Also verify write access works + ok, err = e.Enforce("bob", "data2", "write") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("bob should have write access to data2 after being added to data2_admin role") + } +} + +// TestCachedGFunctionWithMultipleEnforceCalls tests that the g() function cache +// properly invalidates when grouping policies change, even after multiple enforce calls. +func TestCachedGFunctionWithMultipleEnforceCalls(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Make multiple enforce calls to ensure the g() function closure is cached + for i := 0; i < 5; i++ { + ok, enforceErr := e.Enforce("charlie", "data2", "read") + if enforceErr != nil { + t.Fatalf("Enforce failed on iteration %d: %v", i, enforceErr) + } + if ok { + t.Errorf("charlie should not have read access to data2 on iteration %d", i) + } + } + + // Add grouping policy + _, err = e.AddGroupingPolicy("charlie", "data2_admin") + if err != nil { + t.Fatalf("Failed to add grouping policy: %v", err) + } + + // Immediately verify the change took effect + ok, err := e.Enforce("charlie", "data2", "read") + if err != nil { + t.Fatalf("Enforce failed after adding grouping policy: %v", err) + } + if !ok { + t.Error("charlie should have read access to data2 immediately after being added to data2_admin role") + } + + // Make multiple calls to ensure it stays consistent + for i := 0; i < 5; i++ { + ok, enforceErr := e.Enforce("charlie", "data2", "read") + if enforceErr != nil { + t.Fatalf("Enforce failed on iteration %d after policy change: %v", i, enforceErr) + } + if !ok { + t.Errorf("charlie should have read access to data2 on iteration %d after policy change", i) + } + } +} + +// TestCachedGFunctionAfterRemovingGroupingPolicy tests that removing grouping policies +// also properly invalidates the g() function cache. +func TestCachedGFunctionAfterRemovingGroupingPolicy(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // alice initially has the data2_admin role + ok, err := e.Enforce("alice", "data2", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should have read access to data2 initially") + } + + // Remove alice from data2_admin role + _, err = e.RemoveGroupingPolicy("alice", "data2_admin") + if err != nil { + t.Fatalf("Failed to remove grouping policy: %v", err) + } + + // Now alice should not have access to data2 (she only has access to data1) + ok, err = e.Enforce("alice", "data2", "read") + if err != nil { + t.Fatalf("Enforce failed after removing grouping policy: %v", err) + } + if ok { + t.Error("alice should not have read access to data2 after being removed from data2_admin role") + } + + // Verify alice still has access to data1 (direct policy) + ok, err = e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + if !ok { + t.Error("alice should still have read access to data1 (direct policy)") + } +} + +// TestCachedGFunctionAfterBuildRoleLinks tests the specific scenario mentioned in the bug report: +// adding grouping policies and calling BuildRoleLinks() manually should properly invalidate the cache. +func TestCachedGFunctionAfterBuildRoleLinks(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // First, make some enforce calls to ensure the g() function closure is created and cached + // This will cache "bob" NOT having data2_admin role in the g() function's sync.Map + for i := 0; i < 3; i++ { + ok, enforceErr := e.Enforce("bob", "data2", "read") + if enforceErr != nil { + t.Fatalf("Enforce failed on iteration %d: %v", i, enforceErr) + } + if ok { + t.Errorf("bob should not have read access to data2 on iteration %d (before adding role)", i) + } + } + + // Disable autoBuildRoleLinks to manually control when role links are rebuilt + e.EnableAutoBuildRoleLinks(false) + + // Manually add the grouping policy to the model (bypassing BuildIncrementalRoleLinks) + // This simulates the scenario where policies are loaded from database + err = e.model.AddPolicy("g", "g", []string{"bob", "data2_admin"}) + if err != nil { + t.Fatalf("Failed to add grouping policy to model: %v", err) + } + + // Manually build role links as mentioned in the issue + // This is the key part - BuildRoleLinks() should invalidate the matcher map cache + err = e.BuildRoleLinks() + if err != nil { + t.Fatalf("Failed to build role links: %v", err) + } + + // Now bob should have read access to data2 through the data2_admin role + // This is where the bug would manifest - if BuildRoleLinks() doesn't invalidate the cache, + // the old g() function closure with "bob->data2_admin = false" cached will still be used + ok, err := e.Enforce("bob", "data2", "read") + if err != nil { + t.Fatalf("Enforce failed after BuildRoleLinks: %v", err) + } + if !ok { + t.Error("bob should have read access to data2 after BuildRoleLinks() - this indicates the g() function cache was not properly invalidated") + } +} diff --git a/enforcer_cached_synced.go b/enforcer_cached_synced.go new file mode 100644 index 0000000..3ae2613 --- /dev/null +++ b/enforcer_cached_synced.go @@ -0,0 +1,183 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/casbin/casbin/v3/persist/cache" +) + +// SyncedCachedEnforcer wraps Enforcer and provides decision sync cache. +type SyncedCachedEnforcer struct { + *SyncedEnforcer + expireTime time.Duration + cache cache.Cache + enableCache int32 + locker *sync.RWMutex +} + +// NewSyncedCachedEnforcer creates a sync cached enforcer via file or DB. +func NewSyncedCachedEnforcer(params ...interface{}) (*SyncedCachedEnforcer, error) { + e := &SyncedCachedEnforcer{} + var err error + e.SyncedEnforcer, err = NewSyncedEnforcer(params...) + if err != nil { + return nil, err + } + + e.enableCache = 1 + e.cache, _ = cache.NewSyncCache() + e.locker = new(sync.RWMutex) + return e, nil +} + +// EnableCache determines whether to enable cache on Enforce(). When enableCache is enabled, cached result (true | false) will be returned for previous decisions. +func (e *SyncedCachedEnforcer) EnableCache(enableCache bool) { + var enabled int32 + if enableCache { + enabled = 1 + } + atomic.StoreInt32(&e.enableCache, enabled) +} + +// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act). +// if rvals is not string , ignore the cache. +func (e *SyncedCachedEnforcer) Enforce(rvals ...interface{}) (bool, error) { + if atomic.LoadInt32(&e.enableCache) == 0 { + return e.SyncedEnforcer.Enforce(rvals...) + } + + key, ok := e.getKey(rvals...) + if !ok { + return e.SyncedEnforcer.Enforce(rvals...) + } + + if res, err := e.getCachedResult(key); err == nil { + return res, nil + } else if err != cache.ErrNoSuchKey { + return res, err + } + + res, err := e.SyncedEnforcer.Enforce(rvals...) + if err != nil { + return false, err + } + + err = e.setCachedResult(key, res, e.expireTime) + return res, err +} + +func (e *SyncedCachedEnforcer) LoadPolicy() error { + if atomic.LoadInt32(&e.enableCache) != 0 { + if err := e.cache.Clear(); err != nil { + return err + } + } + return e.SyncedEnforcer.LoadPolicy() +} + +func (e *SyncedCachedEnforcer) AddPolicy(params ...interface{}) (bool, error) { + if ok, err := e.checkOneAndRemoveCache(params...); !ok { + return ok, err + } + return e.SyncedEnforcer.AddPolicy(params...) +} + +func (e *SyncedCachedEnforcer) AddPolicies(rules [][]string) (bool, error) { + if ok, err := e.checkManyAndRemoveCache(rules); !ok { + return ok, err + } + return e.SyncedEnforcer.AddPolicies(rules) +} + +func (e *SyncedCachedEnforcer) RemovePolicy(params ...interface{}) (bool, error) { + if ok, err := e.checkOneAndRemoveCache(params...); !ok { + return ok, err + } + return e.SyncedEnforcer.RemovePolicy(params...) +} + +func (e *SyncedCachedEnforcer) RemovePolicies(rules [][]string) (bool, error) { + if ok, err := e.checkManyAndRemoveCache(rules); !ok { + return ok, err + } + return e.SyncedEnforcer.RemovePolicies(rules) +} + +func (e *SyncedCachedEnforcer) getCachedResult(key string) (res bool, err error) { + return e.cache.Get(key) +} + +func (e *SyncedCachedEnforcer) SetExpireTime(expireTime time.Duration) { + e.locker.Lock() + defer e.locker.Unlock() + e.expireTime = expireTime +} + +// SetCache need to be sync cache. +func (e *SyncedCachedEnforcer) SetCache(c cache.Cache) { + e.locker.Lock() + defer e.locker.Unlock() + e.cache = c +} + +func (e *SyncedCachedEnforcer) setCachedResult(key string, res bool, extra ...interface{}) error { + return e.cache.Set(key, res, extra...) +} + +func (e *SyncedCachedEnforcer) getKey(params ...interface{}) (string, bool) { + return GetCacheKey(params...) +} + +// InvalidateCache deletes all the existing cached decisions. +func (e *SyncedCachedEnforcer) InvalidateCache() error { + return e.cache.Clear() +} + +func (e *SyncedCachedEnforcer) checkOneAndRemoveCache(params ...interface{}) (bool, error) { + if atomic.LoadInt32(&e.enableCache) != 0 { + key, ok := e.getKey(params...) + if ok { + if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey { + return false, err + } + } + } + return true, nil +} + +func (e *SyncedCachedEnforcer) checkManyAndRemoveCache(rules [][]string) (bool, error) { + if len(rules) != 0 { + if atomic.LoadInt32(&e.enableCache) != 0 { + for _, rule := range rules { + irule := make([]interface{}, len(rule)) + for i, param := range rule { + irule[i] = param + } + key, _ := e.getKey(irule...) + if err := e.cache.Delete(key); err != nil && err != cache.ErrNoSuchKey { + return false, err + } + } + } + } + return true, nil +} diff --git a/enforcer_cached_synced_test.go b/enforcer_cached_synced_test.go new file mode 100644 index 0000000..1c7da26 --- /dev/null +++ b/enforcer_cached_synced_test.go @@ -0,0 +1,85 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "sync" + "testing" + "time" +) + +func testSyncEnforceCache(t *testing.T, e *SyncedCachedEnforcer, sub string, obj interface{}, act string, res bool) { + t.Helper() + if myRes, _ := e.Enforce(sub, obj, act); myRes != res { + t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res) + } +} + +func TestSyncCache(t *testing.T) { + e, _ := NewSyncedCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + e.expireTime = time.Millisecond + // The cache is enabled by default for NewCachedEnforcer. + g := sync.WaitGroup{} + goThread := 1000 + g.Add(goThread) + for i := 0; i < goThread; i++ { + go func() { + _, _ = e.AddPolicy("alice", "data2", "read") + testSyncEnforceCache(t, e, "alice", "data2", "read", true) + if e.InvalidateCache() != nil { + panic("never reached") + } + g.Done() + }() + } + g.Wait() + _, _ = e.RemovePolicy("alice", "data2", "read") + + testSyncEnforceCache(t, e, "alice", "data1", "read", true) + time.Sleep(time.Millisecond * 2) // coverage for expire + testSyncEnforceCache(t, e, "alice", "data1", "read", true) + + testSyncEnforceCache(t, e, "alice", "data1", "write", false) + testSyncEnforceCache(t, e, "alice", "data2", "read", false) + testSyncEnforceCache(t, e, "alice", "data2", "write", false) + // The cache is enabled, calling RemovePolicy, LoadPolicy or RemovePolicies will + // also operate cached items. + _, _ = e.RemovePolicy("alice", "data1", "read") + + testSyncEnforceCache(t, e, "alice", "data1", "read", false) + testSyncEnforceCache(t, e, "alice", "data1", "write", false) + testSyncEnforceCache(t, e, "alice", "data2", "read", false) + testSyncEnforceCache(t, e, "alice", "data2", "write", false) + + e, _ = NewSyncedCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testSyncEnforceCache(t, e, "alice", "data1", "read", true) + testSyncEnforceCache(t, e, "bob", "data2", "write", true) + testSyncEnforceCache(t, e, "alice", "data2", "read", true) + testSyncEnforceCache(t, e, "alice", "data2", "write", true) + + _, _ = e.RemovePolicies([][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + }) + + testSyncEnforceCache(t, e, "alice", "data1", "read", false) + testSyncEnforceCache(t, e, "bob", "data2", "write", false) + testSyncEnforceCache(t, e, "alice", "data2", "read", true) + testSyncEnforceCache(t, e, "alice", "data2", "write", true) +} diff --git a/enforcer_cached_test.go b/enforcer_cached_test.go new file mode 100644 index 0000000..b404b65 --- /dev/null +++ b/enforcer_cached_test.go @@ -0,0 +1,76 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import "testing" + +func testEnforceCache(t *testing.T, e *CachedEnforcer, sub string, obj interface{}, act string, res bool) { + t.Helper() + if myRes, _ := e.Enforce(sub, obj, act); myRes != res { + t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res) + } +} + +func TestCache(t *testing.T) { + e, _ := NewCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + // The cache is enabled by default for NewCachedEnforcer. + + testEnforceCache(t, e, "alice", "data1", "read", true) + testEnforceCache(t, e, "alice", "data1", "write", false) + testEnforceCache(t, e, "alice", "data2", "read", false) + testEnforceCache(t, e, "alice", "data2", "write", false) + + // The cache is enabled, calling RemovePolicy, LoadPolicy or RemovePolicies will + // also operate cached items. + _, _ = e.RemovePolicy("alice", "data1", "read") + + testEnforceCache(t, e, "alice", "data1", "read", false) + testEnforceCache(t, e, "alice", "data1", "write", false) + testEnforceCache(t, e, "alice", "data2", "read", false) + testEnforceCache(t, e, "alice", "data2", "write", false) + + e, _ = NewCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testEnforceCache(t, e, "alice", "data1", "read", true) + testEnforceCache(t, e, "bob", "data2", "write", true) + testEnforceCache(t, e, "alice", "data2", "read", true) + testEnforceCache(t, e, "alice", "data2", "write", true) + + _, _ = e.RemovePolicies([][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + }) + + testEnforceCache(t, e, "alice", "data1", "read", false) + testEnforceCache(t, e, "bob", "data2", "write", false) + testEnforceCache(t, e, "alice", "data2", "read", true) + testEnforceCache(t, e, "alice", "data2", "write", true) + + e, _ = NewCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + testEnforceCache(t, e, "alice", "data1", "read", true) + testEnforceCache(t, e, "bob", "data2", "write", true) + testEnforceCache(t, e, "alice", "data2", "read", true) + testEnforceCache(t, e, "alice", "data2", "write", true) + + e.ClearPolicy() + + testEnforceCache(t, e, "alice", "data1", "read", false) + testEnforceCache(t, e, "bob", "data2", "write", false) + testEnforceCache(t, e, "alice", "data2", "read", false) + testEnforceCache(t, e, "alice", "data2", "write", false) +} diff --git a/enforcer_context.go b/enforcer_context.go new file mode 100644 index 0000000..b7e4f8a --- /dev/null +++ b/enforcer_context.go @@ -0,0 +1,861 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "context" + "errors" + "fmt" + + Err "github.com/casbin/casbin/v3/errors" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// ContextEnforcer wraps Enforcer and provides context-aware operations. +type ContextEnforcer struct { + *Enforcer + adapterCtx persist.ContextAdapter +} + +// NewContextEnforcer creates a context-aware enforcer via file or DB. +func NewContextEnforcer(params ...interface{}) (IEnforcerContext, error) { + e := &ContextEnforcer{} + var err error + e.Enforcer, err = NewEnforcer(params...) + if err != nil { + return nil, err + } + + if e.adapter != nil { + if contextAdapter, ok := e.adapter.(persist.ContextAdapter); ok { + e.adapterCtx = contextAdapter + } else { + return nil, errors.New("adapter does not support context operations, ContextAdapter interface not implemented") + } + } else { + return nil, errors.New("no adapter provided, ContextEnforcer requires a ContextAdapter") + } + + return e, nil +} + +// LoadPolicyCtx loads all policy rules from the storage with context. +func (e *ContextEnforcer) LoadPolicyCtx(ctx context.Context) error { + newModel, err := e.loadPolicyFromAdapterCtx(ctx, e.model) + if err != nil { + return err + } + err = e.applyModifiedModel(newModel) + if err != nil { + return err + } + return nil +} + +func (e *ContextEnforcer) loadPolicyFromAdapterCtx(ctx context.Context, baseModel model.Model) (model.Model, error) { + newModel := baseModel.Copy() + newModel.ClearPolicy() + + if err := e.adapterCtx.LoadPolicyCtx(ctx, newModel); err != nil && err.Error() != "invalid file path, file path cannot be empty" { + return nil, err + } + + if err := newModel.SortPoliciesBySubjectHierarchy(); err != nil { + return nil, err + } + + if err := newModel.SortPoliciesByPriority(); err != nil { + return nil, err + } + + return newModel, nil +} + +// LoadFilteredPolicyCtx loads all policy rules from the storage with context and filter. +func (e *Enforcer) LoadFilteredPolicyCtx(ctx context.Context, filter interface{}) error { + e.model.ClearPolicy() + return e.loadFilteredPolicyCtx(ctx, filter) +} + +// LoadIncrementalFilteredPolicyCtx append a filtered policy from file/database with context. +func (e *Enforcer) LoadIncrementalFilteredPolicyCtx(ctx context.Context, filter interface{}) error { + return e.loadFilteredPolicyCtx(ctx, filter) +} + +func (e *Enforcer) loadFilteredPolicyCtx(ctx context.Context, filter interface{}) error { + e.invalidateMatcherMap() + + var filteredAdapter persist.ContextFilteredAdapter + + // Attempt to cast the Adapter as a FilteredAdapter + switch adapter := e.adapter.(type) { + case persist.ContextFilteredAdapter: + filteredAdapter = adapter + default: + return errors.New("filtered policies are not supported by this adapter") + } + if err := filteredAdapter.LoadFilteredPolicyCtx(ctx, e.model, filter); err != nil && err.Error() != "invalid file path, file path cannot be empty" { + return err + } + + if err := e.model.SortPoliciesBySubjectHierarchy(); err != nil { + return err + } + + if err := e.model.SortPoliciesByPriority(); err != nil { + return err + } + + e.initRmMap() + e.model.PrintPolicy() + if e.autoBuildRoleLinks { + err := e.BuildRoleLinks() + if err != nil { + return err + } + } + return nil +} + +// IsFilteredCtx returns true if the loaded policy has been filtered with context. +func (e *ContextEnforcer) IsFilteredCtx(ctx context.Context) bool { + if adapter, ok := e.adapter.(persist.ContextFilteredAdapter); ok { + return adapter.IsFilteredCtx(ctx) + } else { + return false + } +} + +func (e *ContextEnforcer) SavePolicyCtx(ctx context.Context) error { + if e.IsFiltered() { + return errors.New("cannot save a filtered policy") + } + if err := e.adapterCtx.SavePolicyCtx(ctx, e.model); err != nil { + return err + } + if e.watcher != nil { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForSavePolicy(e.model) + } else { + err = e.watcher.Update() + } + return err + } + return nil +} + +// AddPolicyCtx adds a policy rule to the storage with context. +func (e *ContextEnforcer) AddPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) { + return e.AddNamedPolicyCtx(ctx, "p", params...) +} + +// AddPoliciesCtx adds policy rules to the storage with context. +func (e *ContextEnforcer) AddPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) { + return e.AddNamedPoliciesCtx(ctx, "p", rules) +} + +// AddNamedPolicyCtx adds a named policy rule to the storage with context. +func (e *ContextEnforcer) AddNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) { + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + strSlice = append(make([]string, 0, len(strSlice)), strSlice...) + return e.addPolicyCtx(ctx, "p", ptype, strSlice) + } + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.addPolicyCtx(ctx, "p", ptype, policy) +} + +// AddNamedPoliciesCtx adds named policy rules to the storage with context. +func (e *ContextEnforcer) AddNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesCtx(ctx, "p", ptype, rules, false) +} + +func (e *ContextEnforcer) AddPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) { + return e.AddNamedPoliciesExCtx(ctx, "p", rules) +} + +func (e *ContextEnforcer) AddNamedPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesCtx(ctx, "p", ptype, rules, true) +} + +// RemovePolicyCtx removes a policy rule from the storage with context. +func (e *ContextEnforcer) RemovePolicyCtx(ctx context.Context, params ...interface{}) (bool, error) { + return e.RemoveNamedPolicyCtx(ctx, "p", params...) +} + +// RemoveNamedPolicyCtx removes a named policy rule from the storage with context. +func (e *ContextEnforcer) RemoveNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) { + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + return e.removePolicyCtx(ctx, "p", ptype, strSlice) + } + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.removePolicyCtx(ctx, "p", ptype, policy) +} + +// RemovePoliciesCtx removes policy rules from the storage with context. +func (e *ContextEnforcer) RemovePoliciesCtx(ctx context.Context, rules [][]string) (bool, error) { + return e.RemoveNamedPoliciesCtx(ctx, "p", rules) +} + +// RemoveNamedPoliciesCtx removes named policy rules from the storage with context. +func (e *ContextEnforcer) RemoveNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) { + return e.removePoliciesCtx(ctx, "p", ptype, rules) +} + +// RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context. +func (e *ContextEnforcer) RemoveFilteredPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) { + return e.RemoveFilteredNamedPolicyCtx(ctx, "p", fieldIndex, fieldValues...) +} + +// RemoveFilteredNamedPolicyCtx removes named policy rules that match the filter from the storage with context. +func (e *ContextEnforcer) RemoveFilteredNamedPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.removeFilteredPolicyCtx(ctx, "p", ptype, fieldIndex, fieldValues) +} + +// UpdatePolicyCtx updates a policy rule in the storage with context. +func (e *ContextEnforcer) UpdatePolicyCtx(ctx context.Context, oldPolicy []string, newPolicy []string) (bool, error) { + return e.UpdateNamedPolicyCtx(ctx, "p", oldPolicy, newPolicy) +} + +// UpdateNamedPolicyCtx updates a named policy rule in the storage with context. +func (e *ContextEnforcer) UpdateNamedPolicyCtx(ctx context.Context, ptype string, p1 []string, p2 []string) (bool, error) { + return e.updatePolicyCtx(ctx, "p", ptype, p1, p2) +} + +// UpdatePoliciesCtx updates policy rules in the storage with context. +func (e *ContextEnforcer) UpdatePoliciesCtx(ctx context.Context, oldPolicies [][]string, newPolicies [][]string) (bool, error) { + return e.UpdateNamedPoliciesCtx(ctx, "p", oldPolicies, newPolicies) +} + +// UpdateNamedPoliciesCtx updates named policy rules in the storage with context. +func (e *ContextEnforcer) UpdateNamedPoliciesCtx(ctx context.Context, ptype string, p1 [][]string, p2 [][]string) (bool, error) { + return e.updatePoliciesCtx(ctx, "p", ptype, p1, p2) +} + +// UpdateFilteredPoliciesCtx updates policy rules that match the filter in the storage with context. +func (e *ContextEnforcer) UpdateFilteredPoliciesCtx(ctx context.Context, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.UpdateFilteredNamedPoliciesCtx(ctx, "p", newPolicies, fieldIndex, fieldValues...) +} + +// UpdateFilteredNamedPoliciesCtx updates named policy rules that match the filter in the storage with context. +func (e *ContextEnforcer) UpdateFilteredNamedPoliciesCtx(ctx context.Context, ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.updateFilteredPoliciesCtx(ctx, "p", ptype, newPolicies, fieldIndex, fieldValues...) +} + +// Grouping Policy Context Methods + +// AddGroupingPolicyCtx adds a grouping policy rule to the storage with context. +func (e *ContextEnforcer) AddGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) { + return e.AddNamedGroupingPolicyCtx(ctx, "g", params...) +} + +// AddGroupingPoliciesCtx adds grouping policy rules to the storage with context. +func (e *ContextEnforcer) AddGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) { + return e.AddNamedGroupingPoliciesCtx(ctx, "g", rules) +} + +func (e *ContextEnforcer) AddGroupingPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) { + return e.AddNamedGroupingPoliciesExCtx(ctx, "g", rules) +} + +// AddNamedGroupingPolicyCtx adds a named grouping policy rule to the storage with context. +func (e *ContextEnforcer) AddNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) { + var ruleAdded bool + var err error + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + ruleAdded, err = e.addPolicyCtx(ctx, "g", ptype, strSlice) + } else { + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + ruleAdded, err = e.addPolicyCtx(ctx, "g", ptype, policy) + } + + return ruleAdded, err +} + +// AddNamedGroupingPoliciesCtx adds named grouping policy rules to the storage with context. +func (e *ContextEnforcer) AddNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesCtx(ctx, "g", ptype, rules, false) +} + +func (e *ContextEnforcer) AddNamedGroupingPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesCtx(ctx, "g", ptype, rules, true) +} + +// RemoveGroupingPolicyCtx removes a grouping policy rule from the storage with context. +func (e *ContextEnforcer) RemoveGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) { + return e.RemoveNamedGroupingPolicyCtx(ctx, "g", params...) +} + +// RemoveNamedGroupingPolicyCtx removes a named grouping policy rule from the storage with context. +func (e *ContextEnforcer) RemoveNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) { + var ruleRemoved bool + var err error + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + ruleRemoved, err = e.removePolicyCtx(ctx, "g", ptype, strSlice) + } else { + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + ruleRemoved, err = e.removePolicyCtx(ctx, "g", ptype, policy) + } + + return ruleRemoved, err +} + +// RemoveGroupingPoliciesCtx removes grouping policy rules from the storage with context. +func (e *ContextEnforcer) RemoveGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) { + return e.RemoveNamedGroupingPoliciesCtx(ctx, "g", rules) +} + +// RemoveNamedGroupingPoliciesCtx removes named grouping policy rules from the storage with context. +func (e *ContextEnforcer) RemoveNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) { + return e.removePoliciesCtx(ctx, "g", ptype, rules) +} + +// RemoveFilteredGroupingPolicyCtx removes grouping policy rules that match the filter from the storage with context. +func (e *ContextEnforcer) RemoveFilteredGroupingPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) { + return e.RemoveFilteredNamedGroupingPolicyCtx(ctx, "g", fieldIndex, fieldValues...) +} + +// RemoveFilteredNamedGroupingPolicyCtx removes named grouping policy rules that match the filter from the storage with context. +func (e *ContextEnforcer) RemoveFilteredNamedGroupingPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.removeFilteredPolicyCtx(ctx, "g", ptype, fieldIndex, fieldValues) +} + +// UpdateGroupingPolicyCtx updates a grouping policy rule in the storage with context. +func (e *ContextEnforcer) UpdateGroupingPolicyCtx(ctx context.Context, oldRule []string, newRule []string) (bool, error) { + return e.UpdateNamedGroupingPolicyCtx(ctx, "g", oldRule, newRule) +} + +// UpdateNamedGroupingPolicyCtx updates a named grouping policy rule in the storage with context. +func (e *ContextEnforcer) UpdateNamedGroupingPolicyCtx(ctx context.Context, ptype string, oldRule []string, newRule []string) (bool, error) { + return e.updatePolicyCtx(ctx, "g", ptype, oldRule, newRule) +} + +// UpdateGroupingPoliciesCtx updates grouping policy rules in the storage with context. +func (e *ContextEnforcer) UpdateGroupingPoliciesCtx(ctx context.Context, oldRules [][]string, newRules [][]string) (bool, error) { + return e.UpdateNamedGroupingPoliciesCtx(ctx, "g", oldRules, newRules) +} + +// UpdateNamedGroupingPoliciesCtx updates named grouping policy rules in the storage with context. +func (e *ContextEnforcer) UpdateNamedGroupingPoliciesCtx(ctx context.Context, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + return e.updatePoliciesCtx(ctx, "g", ptype, oldRules, newRules) +} + +// Self Context Methods (bypass watcher notifications) + +// SelfAddPolicyCtx adds a policy rule to the current policy with context. +func (e *ContextEnforcer) SelfAddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) { + return e.addPolicyWithoutNotifyCtx(ctx, sec, ptype, rule) +} + +// SelfAddPoliciesCtx adds policy rules to the current policy with context. +func (e *ContextEnforcer) SelfAddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, false) +} + +func (e *ContextEnforcer) SelfAddPoliciesExCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, true) +} + +// SelfRemovePolicyCtx removes a policy rule from the current policy with context. +func (e *ContextEnforcer) SelfRemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) { + return e.removePolicyWithoutNotifyCtx(ctx, sec, ptype, rule) +} + +// SelfRemovePoliciesCtx removes policy rules from the current policy with context. +func (e *ContextEnforcer) SelfRemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) { + return e.removePoliciesWithoutNotifyCtx(ctx, sec, ptype, rules) +} + +// SelfRemoveFilteredPolicyCtx removes policy rules that match the filter from the current policy with context. +func (e *ContextEnforcer) SelfRemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.removeFilteredPolicyWithoutNotifyCtx(ctx, sec, ptype, fieldIndex, fieldValues) +} + +// SelfUpdatePolicyCtx updates a policy rule in the current policy with context. +func (e *ContextEnforcer) SelfUpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error) { + return e.updatePolicyWithoutNotifyCtx(ctx, sec, ptype, oldRule, newRule) +} + +// SelfUpdatePoliciesCtx updates policy rules in the current policy with context. +func (e *ContextEnforcer) SelfUpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) (bool, error) { + return e.updatePoliciesWithoutNotifyCtx(ctx, sec, ptype, oldRules, newRules) +} + +// Internal API methods with context support + +// addPolicyWithoutNotifyCtx adds a rule to the current policy with context. +func (e *ContextEnforcer) addPolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.AddPolicies(sec, ptype, [][]string{rule}) + } + + hasPolicy, err := e.model.HasPolicy(sec, ptype, rule) + if hasPolicy || err != nil { + return false, err + } + + if e.shouldPersist() { + if err = e.adapterCtx.AddPolicyCtx(ctx, sec, ptype, rule); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + err = e.model.AddPolicy(sec, ptype, rule) + if err != nil { + return false, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{rule}) + if err != nil { + return true, err + } + } + + return true, nil +} + +// addPoliciesWithoutNotifyCtx adds rules to the current policy with context. +func (e *ContextEnforcer) addPoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.AddPolicies(sec, ptype, rules) + } + + if !autoRemoveRepeat { + hasPolicies, err := e.model.HasPolicies(sec, ptype, rules) + if hasPolicies || err != nil { + return false, err + } + } + + if e.shouldPersist() { + if err := e.adapterCtx.(persist.ContextBatchAdapter).AddPoliciesCtx(ctx, sec, ptype, rules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + err := e.model.AddPolicies(sec, ptype, rules) + if err != nil { + return false, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, rules) + if err != nil { + return true, err + } + + err = e.BuildIncrementalConditionalRoleLinks(model.PolicyAdd, ptype, rules) + if err != nil { + return true, err + } + } + + return true, nil +} + +// removePolicyWithoutNotifyCtx removes a rule from the current policy with context. +func (e *ContextEnforcer) removePolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.RemovePolicies(sec, ptype, [][]string{rule}) + } + + if e.shouldPersist() { + if err := e.adapterCtx.RemovePolicyCtx(ctx, sec, ptype, rule); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + ruleRemoved, err := e.model.RemovePolicy(sec, ptype, rule) + if !ruleRemoved || err != nil { + return ruleRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{rule}) + if err != nil { + return ruleRemoved, err + } + } + + return ruleRemoved, nil +} + +// removePoliciesWithoutNotifyCtx removes rules from the current policy with context. +func (e *ContextEnforcer) removePoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) { + if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); !hasPolicies || err != nil { + return hasPolicies, err + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.RemovePolicies(sec, ptype, rules) + } + + if e.shouldPersist() { + if err := e.adapterCtx.(persist.ContextBatchAdapter).RemovePoliciesCtx(ctx, sec, ptype, rules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + rulesRemoved, err := e.model.RemovePolicies(sec, ptype, rules) + if !rulesRemoved || err != nil { + return rulesRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, rules) + if err != nil { + return rulesRemoved, err + } + } + return rulesRemoved, nil +} + +// removeFilteredPolicyWithoutNotifyCtx removes policy rules that match the filter from the current policy with context. +func (e *ContextEnforcer) removeFilteredPolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) { + if len(fieldValues) == 0 { + return false, Err.ErrInvalidFieldValuesParameter + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + } + + if e.shouldPersist() { + if err := e.adapterCtx.RemoveFilteredPolicyCtx(ctx, sec, ptype, fieldIndex, fieldValues...); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + ruleRemoved, effects, err := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + if !ruleRemoved || err != nil { + return ruleRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, effects) + if err != nil { + return ruleRemoved, err + } + } + + return ruleRemoved, nil +} + +// updatePolicyWithoutNotifyCtx updates a policy rule in the current policy with context. +func (e *ContextEnforcer) updatePolicyWithoutNotifyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.UpdatePolicy(sec, ptype, oldRule, newRule) + } + + if e.shouldPersist() { + if err := e.adapterCtx.(persist.ContextUpdatableAdapter).UpdatePolicyCtx(ctx, sec, ptype, oldRule, newRule); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + ruleUpdated, err := e.model.UpdatePolicy(sec, ptype, oldRule, newRule) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{oldRule}) // remove the old rule + if err != nil { + return ruleUpdated, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{newRule}) // add the new rule + if err != nil { + return ruleUpdated, err + } + } + + return ruleUpdated, nil +} + +func (e *ContextEnforcer) updatePoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + if len(newRules) != len(oldRules) { + return false, fmt.Errorf("the length of oldRules should be equal to the length of newRules, but got the length of oldRules is %d, the length of newRules is %d", len(oldRules), len(newRules)) + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.UpdatePolicies(sec, ptype, oldRules, newRules) + } + + if e.shouldPersist() { + if err := e.adapterCtx.(persist.ContextUpdatableAdapter).UpdatePoliciesCtx(ctx, sec, ptype, oldRules, newRules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + ruleUpdated, err := e.model.UpdatePolicies(sec, ptype, oldRules, newRules) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules + if err != nil { + return ruleUpdated, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules + if err != nil { + return ruleUpdated, err + } + } + + return ruleUpdated, nil +} + +func (e *ContextEnforcer) addPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) { + ok, err := e.addPolicyWithoutNotifyCtx(ctx, sec, ptype, rule) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForAddPolicy(sec, ptype, rule...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *ContextEnforcer) addPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) { + ok, err := e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, autoRemoveRepeat) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForAddPolicies(sec, ptype, rules...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *ContextEnforcer) updatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule []string, newRule []string) (bool, error) { + ok, err := e.updatePolicyWithoutNotifyCtx(ctx, sec, ptype, oldRule, newRule) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicy(sec, ptype, oldRule, newRule) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *ContextEnforcer) updatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + ok, err := e.updatePoliciesWithoutNotifyCtx(ctx, sec, ptype, oldRules, newRules) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *ContextEnforcer) removePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) { + ok, err := e.removePolicyWithoutNotifyCtx(ctx, sec, ptype, rule) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForRemovePolicy(sec, ptype, rule...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *ContextEnforcer) removePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) { + ok, err := e.removePoliciesWithoutNotifyCtx(ctx, sec, ptype, rules) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForRemovePolicies(sec, ptype, rules...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +// removeFilteredPolicy removes rules based on field filters from the current policy. +func (e *ContextEnforcer) removeFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) { + ok, err := e.removeFilteredPolicyWithoutNotifyCtx(ctx, sec, ptype, fieldIndex, fieldValues) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *ContextEnforcer) updateFilteredPoliciesCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + oldRules, err := e.updateFilteredPoliciesWithoutNotifyCtx(ctx, sec, ptype, newRules, fieldIndex, fieldValues...) + ok := len(oldRules) != 0 + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *ContextEnforcer) updateFilteredPoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { + var ( + oldRules [][]string + err error + ) + + if _, err = e.model.GetAssertion(sec, ptype); err != nil { + return oldRules, err + } + + if e.shouldPersist() { + if oldRules, err = e.adapter.(persist.ContextUpdatableAdapter).UpdateFilteredPoliciesCtx(ctx, sec, ptype, newRules, fieldIndex, fieldValues...); err != nil { + if err.Error() != notImplemented { + return nil, err + } + } + // For compatibility, because some adapters return oldRules containing ptype, see https://github.com/casbin/xorm-adapter/issues/49 + for i, oldRule := range oldRules { + if len(oldRules[i]) == len(e.model[sec][ptype].Tokens)+1 { + oldRules[i] = oldRule[1:] + } + } + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return oldRules, e.dispatcher.UpdateFilteredPolicies(sec, ptype, oldRules, newRules) + } + + ruleChanged, err := e.model.RemovePolicies(sec, ptype, oldRules) + if err != nil { + return oldRules, err + } + err = e.model.AddPolicies(sec, ptype, newRules) + if err != nil { + return oldRules, err + } + ruleChanged = ruleChanged && len(newRules) != 0 + if !ruleChanged { + return make([][]string, 0), nil + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules + if err != nil { + return oldRules, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules + if err != nil { + return oldRules, err + } + } + + return oldRules, nil +} diff --git a/enforcer_context_interface.go b/enforcer_context_interface.go new file mode 100644 index 0000000..eebf5c0 --- /dev/null +++ b/enforcer_context_interface.go @@ -0,0 +1,99 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import "context" + +type IEnforcerContext interface { + IEnforcer + + /* Enforcer API */ + LoadPolicyCtx(ctx context.Context) error + LoadFilteredPolicyCtx(ctx context.Context, filter interface{}) error + LoadIncrementalFilteredPolicyCtx(ctx context.Context, filter interface{}) error + IsFilteredCtx(ctx context.Context) bool + SavePolicyCtx(ctx context.Context) error + + /* RBAC API */ + AddRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error) + AddPermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error) + AddPermissionsForUserCtx(ctx context.Context, user string, permissions ...[]string) (bool, error) + DeletePermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error) + DeletePermissionsForUserCtx(ctx context.Context, user string) (bool, error) + + DeleteRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error) + DeleteRolesForUserCtx(ctx context.Context, user string, domain ...string) (bool, error) + DeleteUserCtx(ctx context.Context, user string) (bool, error) + DeleteRoleCtx(ctx context.Context, role string) (bool, error) + DeletePermissionCtx(ctx context.Context, permission ...string) (bool, error) + + /* RBAC API with domains*/ + AddRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error) + DeleteRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error) + DeleteRolesForUserInDomainCtx(ctx context.Context, user string, domain string) (bool, error) + DeleteAllUsersByDomainCtx(ctx context.Context, domain string) (bool, error) + DeleteDomainsCtx(ctx context.Context, domains ...string) (bool, error) + + /* Management API */ + AddPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) + AddPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) + AddNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) + AddNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) + AddPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) + AddNamedPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) + + RemovePolicyCtx(ctx context.Context, params ...interface{}) (bool, error) + RemovePoliciesCtx(ctx context.Context, rules [][]string) (bool, error) + RemoveFilteredPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) + RemoveNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) + RemoveNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) + RemoveFilteredNamedPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) + + AddGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) + AddGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) + AddGroupingPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) + AddNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) + AddNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) + AddNamedGroupingPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) + + RemoveGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) + RemoveGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) + RemoveFilteredGroupingPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) + RemoveNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) + RemoveNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) + RemoveFilteredNamedGroupingPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) + + UpdatePolicyCtx(ctx context.Context, oldPolicy []string, newPolicy []string) (bool, error) + UpdatePoliciesCtx(ctx context.Context, oldPolicies [][]string, newPolicies [][]string) (bool, error) + UpdateFilteredPoliciesCtx(ctx context.Context, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) + + UpdateGroupingPolicyCtx(ctx context.Context, oldRule []string, newRule []string) (bool, error) + UpdateGroupingPoliciesCtx(ctx context.Context, oldRules [][]string, newRules [][]string) (bool, error) + UpdateNamedGroupingPolicyCtx(ctx context.Context, ptype string, oldRule []string, newRule []string) (bool, error) + UpdateNamedGroupingPoliciesCtx(ctx context.Context, ptype string, oldRules [][]string, newRules [][]string) (bool, error) + + /* Management API with autoNotifyWatcher disabled */ + SelfAddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) + SelfAddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) + SelfAddPoliciesExCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) + SelfRemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) + SelfRemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) + SelfRemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) + SelfUpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error) + SelfUpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) (bool, error) +} diff --git a/enforcer_context_test.go b/enforcer_context_test.go new file mode 100644 index 0000000..b3d7e13 --- /dev/null +++ b/enforcer_context_test.go @@ -0,0 +1,235 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "context" + "testing" + "time" +) + +func TestIEnforcerContext_BasicOperations(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx := context.Background() + + err = e.LoadPolicyCtx(ctx) + if err != nil { + t.Fatalf("LoadPolicyCtx failed: %v", err) + } + + added, err := e.AddPolicyCtx(ctx, "eve", "data3", "read") + if err != nil { + t.Fatalf("AddPolicyCtx failed: %v", err) + } + if !added { + t.Error("AddPolicyCtx should return true for new policy") + } + + removed, err := e.RemovePolicyCtx(ctx, "eve", "data3", "read") + if err != nil { + t.Fatalf("RemovePolicyCtx failed: %v", err) + } + if !removed { + t.Error("RemovePolicyCtx should return true for existing policy") + } + + err = e.SavePolicyCtx(ctx) + if err != nil { + t.Fatalf("SavePolicyCtx failed: %v", err) + } +} + +func TestIEnforcerContext_RBACOperations(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx := context.Background() + + added, err := e.AddRoleForUserCtx(ctx, "eve", "data1_admin") + if err != nil { + t.Fatalf("AddRoleForUserCtx failed: %v", err) + } + if !added { + t.Error("AddRoleForUserCtx should return true for new role") + } + + added, err = e.AddPermissionForUserCtx(ctx, "eve", "data3", "write") + if err != nil { + t.Fatalf("AddPermissionForUserCtx failed: %v", err) + } + if !added { + t.Error("AddPermissionForUserCtx should return true for new permission") + } + + deleted, err := e.DeleteRoleForUserCtx(ctx, "eve", "data1_admin") + if err != nil { + t.Fatalf("DeleteRoleForUserCtx failed: %v", err) + } + if !deleted { + t.Error("DeleteRoleForUserCtx should return true for existing role") + } + + deleted, err = e.DeletePermissionForUserCtx(ctx, "eve", "data3", "write") + if err != nil { + t.Fatalf("DeletePermissionForUserCtx failed: %v", err) + } + if !deleted { + t.Error("DeletePermissionForUserCtx should return true for existing permission") + } +} + +func TestIEnforcerContext_BatchOperations(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx := context.Background() + + rules := [][]string{ + {"eve", "data3", "read"}, + {"eve", "data3", "write"}, + } + added, err := e.AddPoliciesCtx(ctx, rules) + if err != nil { + t.Fatalf("AddPoliciesCtx failed: %v", err) + } + if !added { + t.Error("AddPoliciesCtx should return true for new policies") + } + + removed, err := e.RemovePoliciesCtx(ctx, rules) + if err != nil { + t.Fatalf("RemovePoliciesCtx failed: %v", err) + } + if !removed { + t.Error("RemovePoliciesCtx should return true for existing policies") + } +} + +func TestIEnforcerContext_ContextCancellation(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + err = e.LoadPolicyCtx(ctx) + if err != nil { + t.Logf("LoadPolicyCtx with cancelled context returned error: %v", err) + } +} + +func TestIEnforcerContext_ContextTimeout(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + time.Sleep(2 * time.Millisecond) + + _, err = e.AddPolicyCtx(ctx, "test", "data", "read") + if err != nil { + t.Logf("AddPolicyCtx with timeout context returned error: %v", err) + } +} + +func TestIEnforcerContext_GroupingPolicyOperations(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx := context.Background() + + added, err := e.AddGroupingPolicyCtx(ctx, "eve", "data3_admin") + if err != nil { + t.Fatalf("AddGroupingPolicyCtx failed: %v", err) + } + if !added { + t.Error("AddGroupingPolicyCtx should return true for new grouping policy") + } + + removed, err := e.RemoveGroupingPolicyCtx(ctx, "eve", "data3_admin") + if err != nil { + t.Fatalf("RemoveGroupingPolicyCtx failed: %v", err) + } + if !removed { + t.Error("RemoveGroupingPolicyCtx should return true for existing grouping policy") + } +} + +func TestIEnforcerContext_UpdateOperations(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx := context.Background() + + _, err = e.AddPolicyCtx(ctx, "eve", "data3", "read") + if err != nil { + t.Fatalf("AddPolicyCtx failed: %v", err) + } + + updated, err := e.UpdatePolicyCtx(ctx, []string{"eve", "data3", "read"}, []string{"eve", "data3", "write"}) + if err != nil { + t.Fatalf("UpdatePolicyCtx failed: %v", err) + } + if !updated { + t.Error("UpdatePolicyCtx should return true for successful update") + } + + _, _ = e.RemovePolicyCtx(ctx, "eve", "data3", "write") +} + +func TestIEnforcerContext_SelfMethods(t *testing.T) { + e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("NewContextEnforcer failed: %v", err) + } + + ctx := context.Background() + + added, err := e.SelfAddPolicyCtx(ctx, "p", "p", []string{"eve", "data3", "read"}) + if err != nil { + t.Fatalf("SelfAddPolicyCtx failed: %v", err) + } + if !added { + t.Error("SelfAddPolicyCtx should return true for new policy") + } + + removed, err := e.SelfRemovePolicyCtx(ctx, "p", "p", []string{"eve", "data3", "read"}) + if err != nil { + t.Fatalf("SelfRemovePolicyCtx failed: %v", err) + } + if !removed { + t.Error("SelfRemovePolicyCtx should return true for existing policy") + } +} diff --git a/enforcer_distributed.go b/enforcer_distributed.go new file mode 100644 index 0000000..fe73a30 --- /dev/null +++ b/enforcer_distributed.go @@ -0,0 +1,256 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// DistributedEnforcer wraps SyncedEnforcer for dispatcher. +type DistributedEnforcer struct { + *SyncedEnforcer +} + +func NewDistributedEnforcer(params ...interface{}) (*DistributedEnforcer, error) { + e := &DistributedEnforcer{} + var err error + e.SyncedEnforcer, err = NewSyncedEnforcer(params...) + if err != nil { + return nil, err + } + + return e, nil +} + +// SetDispatcher sets the current dispatcher. +func (d *DistributedEnforcer) SetDispatcher(dispatcher persist.Dispatcher) { + d.dispatcher = dispatcher +} + +// AddPoliciesSelf provides a method for dispatcher to add authorization rules to the current policy. +// The function returns the rules affected and error. +func (d *DistributedEnforcer) AddPoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error) { + d.m.Lock() + defer d.m.Unlock() + if shouldPersist != nil && shouldPersist() { + var noExistsPolicy [][]string + for _, rule := range rules { + var hasPolicy bool + hasPolicy, err = d.model.HasPolicy(sec, ptype, rule) + if err != nil { + return nil, err + } + if !hasPolicy { + noExistsPolicy = append(noExistsPolicy, rule) + } + } + + if err = d.adapter.(persist.BatchAdapter).AddPolicies(sec, ptype, noExistsPolicy); err != nil && err.Error() != notImplemented { + return nil, err + } + } + + affected, err = d.model.AddPoliciesWithAffected(sec, ptype, rules) + if err != nil { + return affected, err + } + + if sec == "g" { + err := d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, affected) + if err != nil { + return affected, err + } + } + + return affected, nil +} + +// RemovePoliciesSelf provides a method for dispatcher to remove a set of rules from current policy. +// The function returns the rules affected and error. +func (d *DistributedEnforcer) RemovePoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error) { + d.m.Lock() + defer d.m.Unlock() + if shouldPersist != nil && shouldPersist() { + if err = d.adapter.(persist.BatchAdapter).RemovePolicies(sec, ptype, rules); err != nil { + if err.Error() != notImplemented { + return nil, err + } + } + } + + affected, err = d.model.RemovePoliciesWithAffected(sec, ptype, rules) + if err != nil { + return affected, err + } + + if sec == "g" { + err = d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, affected) + if err != nil { + return affected, err + } + } + + return affected, err +} + +// RemoveFilteredPolicySelf provides a method for dispatcher to remove an authorization rule from the current policy, field filters can be specified. +// The function returns the rules affected and error. +func (d *DistributedEnforcer) RemoveFilteredPolicySelf(shouldPersist func() bool, sec string, ptype string, fieldIndex int, fieldValues ...string) (affected [][]string, err error) { + d.m.Lock() + defer d.m.Unlock() + if shouldPersist != nil && shouldPersist() { + if err = d.adapter.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...); err != nil { + if err.Error() != notImplemented { + return nil, err + } + } + } + + _, affected, err = d.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + if err != nil { + return affected, err + } + + if sec == "g" { + err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, affected) + if err != nil { + return affected, err + } + } + + return affected, nil +} + +// ClearPolicySelf provides a method for dispatcher to clear all rules from the current policy. +func (d *DistributedEnforcer) ClearPolicySelf(shouldPersist func() bool) error { + d.m.Lock() + defer d.m.Unlock() + if shouldPersist != nil && shouldPersist() { + err := d.adapter.SavePolicy(nil) + if err != nil { + return err + } + } + + d.model.ClearPolicy() + + return nil +} + +// UpdatePolicySelf provides a method for dispatcher to update an authorization rule from the current policy. +func (d *DistributedEnforcer) UpdatePolicySelf(shouldPersist func() bool, sec string, ptype string, oldRule, newRule []string) (affected bool, err error) { + d.m.Lock() + defer d.m.Unlock() + if shouldPersist != nil && shouldPersist() { + err = d.adapter.(persist.UpdatableAdapter).UpdatePolicy(sec, ptype, oldRule, newRule) + if err != nil { + return false, err + } + } + + ruleUpdated, err := d.model.UpdatePolicy(sec, ptype, oldRule, newRule) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{oldRule}) // remove the old rule + if err != nil { + return ruleUpdated, err + } + err = d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{newRule}) // add the new rule + if err != nil { + return ruleUpdated, err + } + } + + return ruleUpdated, nil +} + +// UpdatePoliciesSelf provides a method for dispatcher to update a set of authorization rules from the current policy. +func (d *DistributedEnforcer) UpdatePoliciesSelf(shouldPersist func() bool, sec string, ptype string, oldRules, newRules [][]string) (affected bool, err error) { + d.m.Lock() + defer d.m.Unlock() + if shouldPersist != nil && shouldPersist() { + err = d.adapter.(persist.UpdatableAdapter).UpdatePolicies(sec, ptype, oldRules, newRules) + if err != nil { + return false, err + } + } + + ruleUpdated, err := d.model.UpdatePolicies(sec, ptype, oldRules, newRules) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rule + if err != nil { + return ruleUpdated, err + } + err = d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rule + if err != nil { + return ruleUpdated, err + } + } + + return ruleUpdated, nil +} + +// UpdateFilteredPoliciesSelf provides a method for dispatcher to update a set of authorization rules from the current policy. +func (d *DistributedEnforcer) UpdateFilteredPoliciesSelf(shouldPersist func() bool, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + d.m.Lock() + defer d.m.Unlock() + var ( + oldRules [][]string + err error + ) + if shouldPersist != nil && shouldPersist() { + oldRules, err = d.adapter.(persist.UpdatableAdapter).UpdateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues...) + if err != nil { + return false, err + } + } + + ruleChanged, err := d.model.RemovePolicies(sec, ptype, oldRules) + if err != nil { + return ruleChanged, err + } + err = d.model.AddPolicies(sec, ptype, newRules) + if err != nil { + return ruleChanged, err + } + ruleChanged = ruleChanged && len(newRules) != 0 + if !ruleChanged { + return ruleChanged, nil + } + + if sec == "g" { + err := d.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rule + if err != nil { + return ruleChanged, err + } + err = d.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rule + if err != nil { + return ruleChanged, err + } + } + + return true, nil +} diff --git a/enforcer_interface.go b/enforcer_interface.go new file mode 100644 index 0000000..62d37c2 --- /dev/null +++ b/enforcer_interface.go @@ -0,0 +1,184 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "github.com/casbin/casbin/v3/effector" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" + "github.com/casbin/casbin/v3/rbac" + "github.com/casbin/govaluate" +) + +var ( + _ IEnforcer = &Enforcer{} + _ IEnforcer = &SyncedEnforcer{} + _ IEnforcer = &CachedEnforcer{} +) + +// IEnforcer is the API interface of Enforcer. +type IEnforcer interface { + /* Enforcer API */ + InitWithFile(modelPath string, policyPath string) error + InitWithAdapter(modelPath string, adapter persist.Adapter) error + InitWithModelAndAdapter(m model.Model, adapter persist.Adapter) error + LoadModel() error + GetModel() model.Model + SetModel(m model.Model) + GetAdapter() persist.Adapter + SetAdapter(adapter persist.Adapter) + SetWatcher(watcher persist.Watcher) error + GetRoleManager() rbac.RoleManager + SetRoleManager(rm rbac.RoleManager) + SetEffector(eft effector.Effector) + SetAIConfig(config AIConfig) + ClearPolicy() + LoadPolicy() error + LoadFilteredPolicy(filter interface{}) error + LoadIncrementalFilteredPolicy(filter interface{}) error + IsFiltered() bool + SavePolicy() error + EnableEnforce(enable bool) + EnableAutoNotifyWatcher(enable bool) + EnableAutoSave(autoSave bool) + EnableAutoBuildRoleLinks(autoBuildRoleLinks bool) + BuildRoleLinks() error + Enforce(rvals ...interface{}) (bool, error) + EnforceWithMatcher(matcher string, rvals ...interface{}) (bool, error) + EnforceEx(rvals ...interface{}) (bool, []string, error) + EnforceExWithMatcher(matcher string, rvals ...interface{}) (bool, []string, error) + BatchEnforce(requests [][]interface{}) ([]bool, error) + BatchEnforceWithMatcher(matcher string, requests [][]interface{}) ([]bool, error) + Explain(rvals ...interface{}) (string, error) + + /* RBAC API */ + GetRolesForUser(name string, domain ...string) ([]string, error) + GetUsersForRole(name string, domain ...string) ([]string, error) + HasRoleForUser(name string, role string, domain ...string) (bool, error) + AddRoleForUser(user string, role string, domain ...string) (bool, error) + AddPermissionForUser(user string, permission ...string) (bool, error) + AddPermissionsForUser(user string, permissions ...[]string) (bool, error) + DeletePermissionForUser(user string, permission ...string) (bool, error) + DeletePermissionsForUser(user string) (bool, error) + GetPermissionsForUser(user string, domain ...string) ([][]string, error) + HasPermissionForUser(user string, permission ...string) (bool, error) + GetImplicitRolesForUser(name string, domain ...string) ([]string, error) + GetImplicitPermissionsForUser(user string, domain ...string) ([][]string, error) + GetImplicitUsersForPermission(permission ...string) ([]string, error) + DeleteRoleForUser(user string, role string, domain ...string) (bool, error) + DeleteRolesForUser(user string, domain ...string) (bool, error) + DeleteUser(user string) (bool, error) + DeleteRole(role string) (bool, error) + DeletePermission(permission ...string) (bool, error) + + /* RBAC API with domains*/ + GetUsersForRoleInDomain(name string, domain string) []string + GetRolesForUserInDomain(name string, domain string) []string + GetPermissionsForUserInDomain(user string, domain string) [][]string + AddRoleForUserInDomain(user string, role string, domain string) (bool, error) + DeleteRoleForUserInDomain(user string, role string, domain string) (bool, error) + GetAllUsersByDomain(domain string) ([]string, error) + DeleteRolesForUserInDomain(user string, domain string) (bool, error) + DeleteAllUsersByDomain(domain string) (bool, error) + DeleteDomains(domains ...string) (bool, error) + GetAllDomains() ([]string, error) + GetAllRolesByDomain(domain string) ([]string, error) + + /* Management API */ + GetAllSubjects() ([]string, error) + GetAllNamedSubjects(ptype string) ([]string, error) + GetAllObjects() ([]string, error) + GetAllNamedObjects(ptype string) ([]string, error) + GetAllActions() ([]string, error) + GetAllNamedActions(ptype string) ([]string, error) + GetAllRoles() ([]string, error) + GetAllNamedRoles(ptype string) ([]string, error) + GetAllUsers() ([]string, error) + GetPolicy() ([][]string, error) + GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) + GetNamedPolicy(ptype string) ([][]string, error) + GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) + GetGroupingPolicy() ([][]string, error) + GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) + GetNamedGroupingPolicy(ptype string) ([][]string, error) + GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) + HasPolicy(params ...interface{}) (bool, error) + HasNamedPolicy(ptype string, params ...interface{}) (bool, error) + AddPolicy(params ...interface{}) (bool, error) + AddPolicies(rules [][]string) (bool, error) + AddNamedPolicy(ptype string, params ...interface{}) (bool, error) + AddNamedPolicies(ptype string, rules [][]string) (bool, error) + AddPoliciesEx(rules [][]string) (bool, error) + AddNamedPoliciesEx(ptype string, rules [][]string) (bool, error) + RemovePolicy(params ...interface{}) (bool, error) + RemovePolicies(rules [][]string) (bool, error) + RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error) + RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error) + RemoveNamedPolicies(ptype string, rules [][]string) (bool, error) + RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) + HasGroupingPolicy(params ...interface{}) (bool, error) + HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) + AddGroupingPolicy(params ...interface{}) (bool, error) + AddGroupingPolicies(rules [][]string) (bool, error) + AddGroupingPoliciesEx(rules [][]string) (bool, error) + AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) + AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) + AddNamedGroupingPoliciesEx(ptype string, rules [][]string) (bool, error) + RemoveGroupingPolicy(params ...interface{}) (bool, error) + RemoveGroupingPolicies(rules [][]string) (bool, error) + RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error) + RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) + RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) + RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) + AddFunction(name string, function govaluate.ExpressionFunction) + + UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error) + UpdatePolicies(oldPolicies [][]string, newPolicies [][]string) (bool, error) + UpdateFilteredPolicies(newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) + + UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) + UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error) + UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error) + UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error) + + /* Management API with autoNotifyWatcher disabled */ + SelfAddPolicy(sec string, ptype string, rule []string) (bool, error) + SelfAddPolicies(sec string, ptype string, rules [][]string) (bool, error) + SelfAddPoliciesEx(sec string, ptype string, rules [][]string) (bool, error) + SelfRemovePolicy(sec string, ptype string, rule []string) (bool, error) + SelfRemovePolicies(sec string, ptype string, rules [][]string) (bool, error) + SelfRemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) + SelfUpdatePolicy(sec string, ptype string, oldRule, newRule []string) (bool, error) + SelfUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) +} + +var _ IDistributedEnforcer = &DistributedEnforcer{} + +// IDistributedEnforcer defines dispatcher enforcer. +type IDistributedEnforcer interface { + IEnforcer + SetDispatcher(dispatcher persist.Dispatcher) + /* Management API for DistributedEnforcer*/ + AddPoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error) + RemovePoliciesSelf(shouldPersist func() bool, sec string, ptype string, rules [][]string) (affected [][]string, err error) + RemoveFilteredPolicySelf(shouldPersist func() bool, sec string, ptype string, fieldIndex int, fieldValues ...string) (affected [][]string, err error) + ClearPolicySelf(shouldPersist func() bool) error + UpdatePolicySelf(shouldPersist func() bool, sec string, ptype string, oldRule, newRule []string) (affected bool, err error) + UpdatePoliciesSelf(shouldPersist func() bool, sec string, ptype string, oldRules, newRules [][]string) (affected bool, err error) + UpdateFilteredPoliciesSelf(shouldPersist func() bool, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) +} diff --git a/enforcer_json_test.go b/enforcer_json_test.go new file mode 100644 index 0000000..f7348f7 --- /dev/null +++ b/enforcer_json_test.go @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "strings" + "testing" + + "github.com/casbin/casbin/v3/model" +) + +func TestInvalidJsonRequest(t *testing.T) { + modelText := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub.Name == " " +` + + m, err := model.NewModelFromString(modelText) + if err != nil { + t.Fatalf("Failed to create model: %v", err) + } + e, err := NewEnforcer(m) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + e.EnableAcceptJsonRequest(true) + + // Test with invalid JSON (contains \x escape sequence which is not valid in JSON) + invalidJSON := `{"Name": "\x20"}` + _, err = e.Enforce(invalidJSON, "obj", "read") + if err == nil { + t.Fatalf("Expected error for invalid JSON, got nil") + } + if !strings.Contains(err.Error(), "failed to parse JSON parameter") { + t.Fatalf("Expected error message to contain 'failed to parse JSON parameter', got: %v", err) + } + + // Test with valid JSON - should work + validJSON := `{"Name": " "}` + res, err := e.Enforce(validJSON, "obj", "read") + if err != nil { + t.Fatalf("Valid JSON should not return error: %v", err) + } + if !res { + t.Fatalf("Expected true for valid JSON with matching Name") + } + + // Test with plain string (doesn't start with { or [) - should not try to parse as JSON + plainString := "alice" + _, err = e.Enforce(plainString, "obj", "read") + // This will fail because plainString is not a struct with Name field, + // but it shouldn't fail with JSON parsing error + if err != nil && strings.Contains(err.Error(), "failed to parse JSON parameter") { + t.Fatalf("Plain string should not trigger JSON parsing error: %v", err) + } +} diff --git a/enforcer_synced.go b/enforcer_synced.go new file mode 100644 index 0000000..772b391 --- /dev/null +++ b/enforcer_synced.go @@ -0,0 +1,689 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/casbin/govaluate" + + "github.com/casbin/casbin/v3/persist" + "github.com/casbin/casbin/v3/rbac" +) + +// SyncedEnforcer wraps Enforcer and provides synchronized access. +type SyncedEnforcer struct { + *Enforcer + m sync.RWMutex + stopAutoLoad chan struct{} + autoLoadRunning int32 +} + +// NewSyncedEnforcer creates a synchronized enforcer via file or DB. +func NewSyncedEnforcer(params ...interface{}) (*SyncedEnforcer, error) { + e := &SyncedEnforcer{} + var err error + e.Enforcer, err = NewEnforcer(params...) + if err != nil { + return nil, err + } + + e.stopAutoLoad = make(chan struct{}, 1) + e.autoLoadRunning = 0 + return e, nil +} + +// GetLock return the private RWMutex lock. +func (e *SyncedEnforcer) GetLock() *sync.RWMutex { + return &e.m +} + +// GetRoleManager gets the current role manager with synchronization. +func (e *SyncedEnforcer) GetRoleManager() rbac.RoleManager { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetRoleManager() +} + +// GetNamedRoleManager gets the role manager for the named policy with synchronization. +func (e *SyncedEnforcer) GetNamedRoleManager(ptype string) rbac.RoleManager { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetNamedRoleManager(ptype) +} + +// SetRoleManager sets the current role manager with synchronization. +func (e *SyncedEnforcer) SetRoleManager(rm rbac.RoleManager) { + e.m.Lock() + defer e.m.Unlock() + e.Enforcer.SetRoleManager(rm) +} + +// SetNamedRoleManager sets the role manager for the named policy with synchronization. +func (e *SyncedEnforcer) SetNamedRoleManager(ptype string, rm rbac.RoleManager) { + e.m.Lock() + defer e.m.Unlock() + e.Enforcer.SetNamedRoleManager(ptype, rm) +} + +// IsAutoLoadingRunning check if SyncedEnforcer is auto loading policies. +func (e *SyncedEnforcer) IsAutoLoadingRunning() bool { + return atomic.LoadInt32(&(e.autoLoadRunning)) != 0 +} + +// StartAutoLoadPolicy starts a go routine that will every specified duration call LoadPolicy. +func (e *SyncedEnforcer) StartAutoLoadPolicy(d time.Duration) { + // Don't start another goroutine if there is already one running + if !atomic.CompareAndSwapInt32(&e.autoLoadRunning, 0, 1) { + return + } + + ticker := time.NewTicker(d) + go func() { + defer func() { + ticker.Stop() + atomic.StoreInt32(&(e.autoLoadRunning), int32(0)) + }() + n := 1 + for { + select { + case <-ticker.C: + // error intentionally ignored + _ = e.LoadPolicy() + // Uncomment this line to see when the policy is loaded. + // log.Print("Load policy for time: ", n) + n++ + case <-e.stopAutoLoad: + return + } + } + }() +} + +// StopAutoLoadPolicy causes the go routine to exit. +func (e *SyncedEnforcer) StopAutoLoadPolicy() { + if e.IsAutoLoadingRunning() { + e.stopAutoLoad <- struct{}{} + } +} + +// SetWatcher sets the current watcher. +func (e *SyncedEnforcer) SetWatcher(watcher persist.Watcher) error { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SetWatcher(watcher) +} + +// LoadModel reloads the model from the model CONF file. +func (e *SyncedEnforcer) LoadModel() error { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.LoadModel() +} + +// ClearPolicy clears all policy. +func (e *SyncedEnforcer) ClearPolicy() { + e.m.Lock() + defer e.m.Unlock() + e.Enforcer.ClearPolicy() +} + +// LoadPolicy reloads the policy from file/database. +func (e *SyncedEnforcer) LoadPolicy() error { + e.m.RLock() + newModel, err := e.loadPolicyFromAdapter(e.model) + e.m.RUnlock() + if err != nil { + return err + } + e.m.Lock() + err = e.applyModifiedModel(newModel) + e.m.Unlock() + if err != nil { + return err + } + return nil +} + +// LoadFilteredPolicy reloads a filtered policy from file/database. +func (e *SyncedEnforcer) LoadFilteredPolicy(filter interface{}) error { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.LoadFilteredPolicy(filter) +} + +// LoadIncrementalFilteredPolicy reloads a filtered policy from file/database. +func (e *SyncedEnforcer) LoadIncrementalFilteredPolicy(filter interface{}) error { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.LoadIncrementalFilteredPolicy(filter) +} + +// SavePolicy saves the current policy (usually after changed with Casbin API) back to file/database. +func (e *SyncedEnforcer) SavePolicy() error { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SavePolicy() +} + +// BuildRoleLinks manually rebuild the role inheritance relations. +func (e *SyncedEnforcer) BuildRoleLinks() error { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.BuildRoleLinks() +} + +// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act). +func (e *SyncedEnforcer) Enforce(rvals ...interface{}) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.Enforce(rvals...) +} + +// EnforceWithMatcher use a custom matcher to decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (matcher, sub, obj, act), use model matcher by default when matcher is "". +func (e *SyncedEnforcer) EnforceWithMatcher(matcher string, rvals ...interface{}) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.EnforceWithMatcher(matcher, rvals...) +} + +// EnforceEx explain enforcement by informing matched rules. +func (e *SyncedEnforcer) EnforceEx(rvals ...interface{}) (bool, []string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.EnforceEx(rvals...) +} + +// EnforceExWithMatcher use a custom matcher and explain enforcement by informing matched rules. +func (e *SyncedEnforcer) EnforceExWithMatcher(matcher string, rvals ...interface{}) (bool, []string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.EnforceExWithMatcher(matcher, rvals...) +} + +// BatchEnforce enforce in batches. +func (e *SyncedEnforcer) BatchEnforce(requests [][]interface{}) ([]bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.BatchEnforce(requests) +} + +// BatchEnforceWithMatcher enforce with matcher in batches. +func (e *SyncedEnforcer) BatchEnforceWithMatcher(matcher string, requests [][]interface{}) ([]bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.BatchEnforceWithMatcher(matcher, requests) +} + +// GetAllSubjects gets the list of subjects that show up in the current policy. +func (e *SyncedEnforcer) GetAllSubjects() ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllSubjects() +} + +// GetAllNamedSubjects gets the list of subjects that show up in the current named policy. +func (e *SyncedEnforcer) GetAllNamedSubjects(ptype string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllNamedSubjects(ptype) +} + +// GetAllObjects gets the list of objects that show up in the current policy. +func (e *SyncedEnforcer) GetAllObjects() ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllObjects() +} + +// GetAllNamedObjects gets the list of objects that show up in the current named policy. +func (e *SyncedEnforcer) GetAllNamedObjects(ptype string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllNamedObjects(ptype) +} + +// GetAllActions gets the list of actions that show up in the current policy. +func (e *SyncedEnforcer) GetAllActions() ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllActions() +} + +// GetAllNamedActions gets the list of actions that show up in the current named policy. +func (e *SyncedEnforcer) GetAllNamedActions(ptype string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllNamedActions(ptype) +} + +// GetAllRoles gets the list of roles that show up in the current policy. +func (e *SyncedEnforcer) GetAllRoles() ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllRoles() +} + +// GetAllNamedRoles gets the list of roles that show up in the current named policy. +func (e *SyncedEnforcer) GetAllNamedRoles(ptype string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllNamedRoles(ptype) +} + +// GetAllUsers gets the list of users that show up in the current policy. +func (e *SyncedEnforcer) GetAllUsers() ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetAllUsers() +} + +// GetPolicy gets all the authorization rules in the policy. +func (e *SyncedEnforcer) GetPolicy() ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetPolicy() +} + +// GetFilteredPolicy gets all the authorization rules in the policy, field filters can be specified. +func (e *SyncedEnforcer) GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetFilteredPolicy(fieldIndex, fieldValues...) +} + +// GetNamedPolicy gets all the authorization rules in the named policy. +func (e *SyncedEnforcer) GetNamedPolicy(ptype string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetNamedPolicy(ptype) +} + +// GetFilteredNamedPolicy gets all the authorization rules in the named policy, field filters can be specified. +func (e *SyncedEnforcer) GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetFilteredNamedPolicy(ptype, fieldIndex, fieldValues...) +} + +// GetGroupingPolicy gets all the role inheritance rules in the policy. +func (e *SyncedEnforcer) GetGroupingPolicy() ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetGroupingPolicy() +} + +// GetFilteredGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified. +func (e *SyncedEnforcer) GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetFilteredGroupingPolicy(fieldIndex, fieldValues...) +} + +// GetNamedGroupingPolicy gets all the role inheritance rules in the policy. +func (e *SyncedEnforcer) GetNamedGroupingPolicy(ptype string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetNamedGroupingPolicy(ptype) +} + +// GetFilteredNamedGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified. +func (e *SyncedEnforcer) GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetFilteredNamedGroupingPolicy(ptype, fieldIndex, fieldValues...) +} + +// HasPolicy determines whether an authorization rule exists. +func (e *SyncedEnforcer) HasPolicy(params ...interface{}) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.HasPolicy(params...) +} + +// HasNamedPolicy determines whether a named authorization rule exists. +func (e *SyncedEnforcer) HasNamedPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.HasNamedPolicy(ptype, params...) +} + +// AddPolicy adds an authorization rule to the current policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *SyncedEnforcer) AddPolicy(params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPolicy(params...) +} + +// AddPolicies adds authorization rules to the current policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding rule by adding the new rule. +func (e *SyncedEnforcer) AddPolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPolicies(rules) +} + +// AddPoliciesEx adds authorization rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddPoliciesEx(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPoliciesEx(rules) +} + +// AddNamedPolicy adds an authorization rule to the current named policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *SyncedEnforcer) AddNamedPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedPolicy(ptype, params...) +} + +// AddNamedPolicies adds authorization rules to the current named policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding by adding the new rule. +func (e *SyncedEnforcer) AddNamedPolicies(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedPolicies(ptype, rules) +} + +// AddNamedPoliciesEx adds authorization rules to the current named policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddNamedPoliciesEx(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedPoliciesEx(ptype, rules) +} + +// RemovePolicy removes an authorization rule from the current policy. +func (e *SyncedEnforcer) RemovePolicy(params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemovePolicy(params...) +} + +// UpdatePolicy updates an authorization rule from the current policy. +func (e *SyncedEnforcer) UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdatePolicy(oldPolicy, newPolicy) +} + +func (e *SyncedEnforcer) UpdateNamedPolicy(ptype string, p1 []string, p2 []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedPolicy(ptype, p1, p2) +} + +// UpdatePolicies updates authorization rules from the current policies. +func (e *SyncedEnforcer) UpdatePolicies(oldPolices [][]string, newPolicies [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdatePolicies(oldPolices, newPolicies) +} + +func (e *SyncedEnforcer) UpdateNamedPolicies(ptype string, p1 [][]string, p2 [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedPolicies(ptype, p1, p2) +} + +func (e *SyncedEnforcer) UpdateFilteredPolicies(newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateFilteredPolicies(newPolicies, fieldIndex, fieldValues...) +} + +func (e *SyncedEnforcer) UpdateFilteredNamedPolicies(ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateFilteredNamedPolicies(ptype, newPolicies, fieldIndex, fieldValues...) +} + +// RemovePolicies removes authorization rules from the current policy. +func (e *SyncedEnforcer) RemovePolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemovePolicies(rules) +} + +// RemoveFilteredPolicy removes an authorization rule from the current policy, field filters can be specified. +func (e *SyncedEnforcer) RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveFilteredPolicy(fieldIndex, fieldValues...) +} + +// RemoveNamedPolicy removes an authorization rule from the current named policy. +func (e *SyncedEnforcer) RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveNamedPolicy(ptype, params...) +} + +// RemoveNamedPolicies removes authorization rules from the current named policy. +func (e *SyncedEnforcer) RemoveNamedPolicies(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveNamedPolicies(ptype, rules) +} + +// RemoveFilteredNamedPolicy removes an authorization rule from the current named policy, field filters can be specified. +func (e *SyncedEnforcer) RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveFilteredNamedPolicy(ptype, fieldIndex, fieldValues...) +} + +// HasGroupingPolicy determines whether a role inheritance rule exists. +func (e *SyncedEnforcer) HasGroupingPolicy(params ...interface{}) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.HasGroupingPolicy(params...) +} + +// HasNamedGroupingPolicy determines whether a named role inheritance rule exists. +func (e *SyncedEnforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.HasNamedGroupingPolicy(ptype, params...) +} + +// AddGroupingPolicy adds a role inheritance rule to the current policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *SyncedEnforcer) AddGroupingPolicy(params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddGroupingPolicy(params...) +} + +// AddGroupingPolicies adds role inheritance rulea to the current policy. +// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. +// Otherwise the function returns true for the corresponding policy rule by adding the new rule. +func (e *SyncedEnforcer) AddGroupingPolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddGroupingPolicies(rules) +} + +// AddGroupingPoliciesEx adds role inheritance rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddGroupingPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddGroupingPoliciesEx(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddGroupingPoliciesEx(rules) +} + +// AddNamedGroupingPolicy adds a named role inheritance rule to the current policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *SyncedEnforcer) AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedGroupingPolicy(ptype, params...) +} + +// AddNamedGroupingPolicies adds named role inheritance rules to the current policy. +// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. +// Otherwise the function returns true for the corresponding policy rule by adding the new rule. +func (e *SyncedEnforcer) AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedGroupingPolicies(ptype, rules) +} + +// AddNamedGroupingPoliciesEx adds named role inheritance rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedGroupingPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddNamedGroupingPoliciesEx(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedGroupingPoliciesEx(ptype, rules) +} + +// RemoveGroupingPolicy removes a role inheritance rule from the current policy. +func (e *SyncedEnforcer) RemoveGroupingPolicy(params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveGroupingPolicy(params...) +} + +// RemoveGroupingPolicies removes role inheritance rules from the current policy. +func (e *SyncedEnforcer) RemoveGroupingPolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveGroupingPolicies(rules) +} + +// RemoveFilteredGroupingPolicy removes a role inheritance rule from the current policy, field filters can be specified. +func (e *SyncedEnforcer) RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveFilteredGroupingPolicy(fieldIndex, fieldValues...) +} + +// RemoveNamedGroupingPolicy removes a role inheritance rule from the current named policy. +func (e *SyncedEnforcer) RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveNamedGroupingPolicy(ptype, params...) +} + +// RemoveNamedGroupingPolicies removes role inheritance rules from the current named policy. +func (e *SyncedEnforcer) RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveNamedGroupingPolicies(ptype, rules) +} + +func (e *SyncedEnforcer) UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateGroupingPolicy(oldRule, newRule) +} + +func (e *SyncedEnforcer) UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateGroupingPolicies(oldRules, newRules) +} + +func (e *SyncedEnforcer) UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedGroupingPolicy(ptype, oldRule, newRule) +} + +func (e *SyncedEnforcer) UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedGroupingPolicies(ptype, oldRules, newRules) +} + +// RemoveFilteredNamedGroupingPolicy removes a role inheritance rule from the current named policy, field filters can be specified. +func (e *SyncedEnforcer) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveFilteredNamedGroupingPolicy(ptype, fieldIndex, fieldValues...) +} + +// AddFunction adds a customized function. +func (e *SyncedEnforcer) AddFunction(name string, function govaluate.ExpressionFunction) { + e.m.Lock() + defer e.m.Unlock() + e.Enforcer.AddFunction(name, function) +} + +func (e *SyncedEnforcer) SelfAddPolicy(sec string, ptype string, rule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfAddPolicy(sec, ptype, rule) +} + +func (e *SyncedEnforcer) SelfAddPolicies(sec string, ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfAddPolicies(sec, ptype, rules) +} + +func (e *SyncedEnforcer) SelfAddPoliciesEx(sec string, ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfAddPoliciesEx(sec, ptype, rules) +} + +func (e *SyncedEnforcer) SelfRemovePolicy(sec string, ptype string, rule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfRemovePolicy(sec, ptype, rule) +} + +func (e *SyncedEnforcer) SelfRemovePolicies(sec string, ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfRemovePolicies(sec, ptype, rules) +} + +func (e *SyncedEnforcer) SelfRemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) +} + +func (e *SyncedEnforcer) SelfUpdatePolicy(sec string, ptype string, oldRule, newRule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfUpdatePolicy(sec, ptype, oldRule, newRule) +} + +func (e *SyncedEnforcer) SelfUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfUpdatePolicies(sec, ptype, oldRules, newRules) +} diff --git a/enforcer_synced_test.go b/enforcer_synced_test.go new file mode 100644 index 0000000..5bf02d0 --- /dev/null +++ b/enforcer_synced_test.go @@ -0,0 +1,537 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "sort" + "testing" + "time" + + "github.com/casbin/casbin/v3/errors" + "github.com/casbin/casbin/v3/util" +) + +func testEnforceSync(t *testing.T, e *SyncedEnforcer, sub string, obj interface{}, act string, res bool) { + t.Helper() + if myRes, _ := e.Enforce(sub, obj, act); myRes != res { + t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res) + } +} + +func TestSync(t *testing.T) { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + // Start reloading the policy every 200 ms. + e.StartAutoLoadPolicy(time.Millisecond * 200) + + testEnforceSync(t, e, "alice", "data1", "read", true) + testEnforceSync(t, e, "alice", "data1", "write", false) + testEnforceSync(t, e, "alice", "data2", "read", false) + testEnforceSync(t, e, "alice", "data2", "write", false) + testEnforceSync(t, e, "bob", "data1", "read", false) + testEnforceSync(t, e, "bob", "data1", "write", false) + testEnforceSync(t, e, "bob", "data2", "read", false) + testEnforceSync(t, e, "bob", "data2", "write", true) + + // Simulate a policy change + e.ClearPolicy() + testEnforceSync(t, e, "bob", "data2", "write", false) + + // Wait for at least one sync + time.Sleep(time.Millisecond * 300) + + testEnforceSync(t, e, "bob", "data2", "write", true) + + // Stop the reloading policy periodically. + e.StopAutoLoadPolicy() +} + +func TestStopAutoLoadPolicy(t *testing.T) { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + e.StartAutoLoadPolicy(5 * time.Millisecond) + if !e.IsAutoLoadingRunning() { + t.Error("auto load is not running") + } + e.StopAutoLoadPolicy() + // Need a moment, to exit goroutine + time.Sleep(10 * time.Millisecond) + if e.IsAutoLoadingRunning() { + t.Error("auto load is still running") + } +} + +func testSyncedEnforcerGetPolicy(t *testing.T, e *SyncedEnforcer, res [][]string) { + t.Helper() + myRes, err := e.GetPolicy() + if err != nil { + t.Error(err) + } + + if !util.SortedArray2DEquals(res, myRes) { + t.Error("Policy: ", myRes, ", supposed to be ", res) + } else { + t.Log("Policy: ", myRes) + } +} + +func TestSyncedEnforcerSelfAddPolicy(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }() + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + } +} + +func TestSyncedEnforcerSelfAddPolicies(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { + _, _ = e.SelfAddPolicies("p", "p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) + }() + go func() { + _, _ = e.SelfAddPolicies("p", "p", [][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}}) + }() + go func() { + _, _ = e.SelfAddPolicies("p", "p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) + }() + + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + } +} + +func TestSyncedEnforcerSelfAddPoliciesEx(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { + _, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) + }() + go func() { + _, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}}) + }() + go func() { + _, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}}) + }() + go func() { + _, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}}) + }() + go func() { + _, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) + }() + go func() { + _, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user6", "data6", "read"}, {"user1", "data1", "read"}}) + }() + + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + } +} + +func TestSyncedEnforcerSelfRemovePolicy(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }() + + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + + go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user1", "data1", "read"}) }() + go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user2", "data2", "read"}) }() + go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user3", "data3", "read"}) }() + go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user4", "data4", "read"}) }() + go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user5", "data5", "read"}) }() + go func() { _, _ = e.SelfRemovePolicy("p", "p", []string{"user6", "data6", "read"}) }() + + time.Sleep(100 * time.Millisecond) + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + }) + } +} + +func TestSyncedEnforcerSelfRemovePolicies(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }() + + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + + go func() { + _, _ = e.SelfRemovePolicies("p", "p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) + }() + go func() { + _, _ = e.SelfRemovePolicies("p", "p", [][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}}) + }() + go func() { + _, _ = e.SelfRemovePolicies("p", "p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) + }() + + time.Sleep(100 * time.Millisecond) + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + }) + } +} + +func TestSyncedEnforcerSelfRemoveFilteredPolicy(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user7", "data7", "write"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user8", "data8", "write"}) }() + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + {"user7", "data7", "write"}, + {"user8", "data8", "write"}, + }) + + go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user1") }() + go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user2") }() + go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 1, "data3") }() + go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 1, "data4") }() + go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user5") }() + go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 0, "user6") }() + go func() { _, _ = e.SelfRemoveFilteredPolicy("p", "p", 2, "write") }() + + time.Sleep(100 * time.Millisecond) + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + }) + } +} + +func TestSyncedEnforcerSelfUpdatePolicy(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }() + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + + go func() { + _, _ = e.SelfUpdatePolicy("p", "p", []string{"user1", "data1", "read"}, []string{"user1", "data1", "write"}) + }() + go func() { + _, _ = e.SelfUpdatePolicy("p", "p", []string{"user2", "data2", "read"}, []string{"user2", "data2", "write"}) + }() + go func() { + _, _ = e.SelfUpdatePolicy("p", "p", []string{"user3", "data3", "read"}, []string{"user3", "data3", "write"}) + }() + go func() { + _, _ = e.SelfUpdatePolicy("p", "p", []string{"user4", "data4", "read"}, []string{"user4", "data4", "write"}) + }() + go func() { + _, _ = e.SelfUpdatePolicy("p", "p", []string{"user5", "data5", "read"}, []string{"user5", "data5", "write"}) + }() + go func() { + _, _ = e.SelfUpdatePolicy("p", "p", []string{"user6", "data6", "read"}, []string{"user6", "data6", "write"}) + }() + + time.Sleep(100 * time.Millisecond) + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "write"}, + {"user2", "data2", "write"}, + {"user3", "data3", "write"}, + {"user4", "data4", "write"}, + {"user5", "data5", "write"}, + {"user6", "data6", "write"}, + }) + } +} + +func TestSyncedEnforcerSelfUpdatePolicies(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user1", "data1", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user2", "data2", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user3", "data3", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user4", "data4", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user5", "data5", "read"}) }() + go func() { _, _ = e.SelfAddPolicy("p", "p", []string{"user6", "data6", "read"}) }() + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + + go func() { + _, _ = e.SelfUpdatePolicies("p", "p", + [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}, + [][]string{{"user1", "data1", "write"}, {"user2", "data2", "write"}}) + }() + + go func() { + _, _ = e.SelfUpdatePolicies("p", "p", + [][]string{{"user3", "data3", "read"}, {"user4", "data4", "read"}}, + [][]string{{"user3", "data3", "write"}, {"user4", "data4", "write"}}) + }() + + go func() { + _, _ = e.SelfUpdatePolicies("p", "p", + [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}, + [][]string{{"user5", "data5", "write"}, {"user6", "data6", "write"}}) + }() + + time.Sleep(100 * time.Millisecond) + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "write"}, + {"user2", "data2", "write"}, + {"user3", "data3", "write"}, + {"user4", "data4", "write"}, + {"user5", "data5", "write"}, + {"user6", "data6", "write"}, + }) + } +} + +func TestSyncedEnforcerAddPoliciesEx(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { _, _ = e.AddPoliciesEx([][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) }() + go func() { _, _ = e.AddPoliciesEx([][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}}) }() + go func() { _, _ = e.AddPoliciesEx([][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}}) }() + go func() { _, _ = e.AddPoliciesEx([][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) }() + go func() { _, _ = e.AddPoliciesEx([][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) }() + go func() { _, _ = e.AddPoliciesEx([][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}}) }() + go func() { _, _ = e.AddPoliciesEx([][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}}) }() + go func() { _, _ = e.AddPoliciesEx([][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) }() + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + } +} + +func TestSyncedEnforcerAddNamedPoliciesEx(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) + }() + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}}) + }() + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}}) + }() + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) + }() + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) + }() + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user2", "data2", "read"}, {"user3", "data3", "read"}}) + }() + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user4", "data4", "read"}, {"user5", "data5", "read"}}) + }() + go func() { + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user5", "data5", "read"}, {"user6", "data6", "read"}}) + }() + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"user1", "data1", "read"}, + {"user2", "data2", "read"}, + {"user3", "data3", "read"}, + {"user4", "data4", "read"}, + {"user5", "data5", "read"}, + {"user6", "data6", "read"}, + }) + } +} + +func testSyncedEnforcerGetUsers(t *testing.T, e *SyncedEnforcer, res []string, name string, domain ...string) { + t.Helper() + myRes, err := e.GetUsersForRole(name, domain...) + myResCopy := make([]string, len(myRes)) + copy(myResCopy, myRes) + sort.Strings(myRes) + sort.Strings(res) + switch err { + case nil: + break + case errors.ErrNameNotFound: + t.Log("No name found") + default: + t.Error("Users for ", name, " could not be fetched: ", err.Error()) + } + t.Log("Users for ", name, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Users for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func TestSyncedEnforcerAddGroupingPoliciesEx(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + e.ClearPolicy() + + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user1", "member"}, {"user2", "member"}}) }() + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user2", "member"}, {"user3", "member"}}) }() + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user4", "member"}, {"user5", "member"}}) }() + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user5", "member"}, {"user6", "member"}}) }() + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user1", "member"}, {"user2", "member"}}) }() + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user2", "member"}, {"user3", "member"}}) }() + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user4", "member"}, {"user5", "member"}}) }() + go func() { _, _ = e.AddGroupingPoliciesEx([][]string{{"user5", "member"}, {"user6", "member"}}) }() + + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetUsers(t, e, []string{"user1", "user2", "user3", "user4", "user5", "user6"}, "member") + } +} + +func TestSyncedEnforcerAddNamedGroupingPoliciesEx(t *testing.T) { + for i := 0; i < 10; i++ { + e, _ := NewSyncedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + e.ClearPolicy() + + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user1", "member"}, {"user2", "member"}}) }() + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user2", "member"}, {"user3", "member"}}) }() + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user4", "member"}, {"user5", "member"}}) }() + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user5", "member"}, {"user6", "member"}}) }() + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user1", "member"}, {"user2", "member"}}) }() + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user2", "member"}, {"user3", "member"}}) }() + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user4", "member"}, {"user5", "member"}}) }() + go func() { _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user5", "member"}, {"user6", "member"}}) }() + + time.Sleep(100 * time.Millisecond) + + testSyncedEnforcerGetUsers(t, e, []string{"user1", "user2", "user3", "user4", "user5", "user6"}, "member") + } +} diff --git a/enforcer_test.go b/enforcer_test.go new file mode 100644 index 0000000..918f38e --- /dev/null +++ b/enforcer_test.go @@ -0,0 +1,806 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "strings" + "sync" + "testing" + + "github.com/casbin/casbin/v3/detector" + "github.com/casbin/casbin/v3/model" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" + "github.com/casbin/casbin/v3/util" +) + +func TestKeyMatchModelInMemory(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("e", "e", "some(where (p.eft == allow))") + m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)") + + a := fileadapter.NewAdapter("examples/keymatch_policy.csv") + + e, _ := NewEnforcer(m, a) + + testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true) + testEnforce(t, e, "alice", "/alice_data/resource1", "POST", true) + testEnforce(t, e, "alice", "/alice_data/resource2", "GET", true) + testEnforce(t, e, "alice", "/alice_data/resource2", "POST", false) + testEnforce(t, e, "alice", "/bob_data/resource1", "GET", false) + testEnforce(t, e, "alice", "/bob_data/resource1", "POST", false) + testEnforce(t, e, "alice", "/bob_data/resource2", "GET", false) + testEnforce(t, e, "alice", "/bob_data/resource2", "POST", false) + + testEnforce(t, e, "bob", "/alice_data/resource1", "GET", false) + testEnforce(t, e, "bob", "/alice_data/resource1", "POST", false) + testEnforce(t, e, "bob", "/alice_data/resource2", "GET", true) + testEnforce(t, e, "bob", "/alice_data/resource2", "POST", false) + testEnforce(t, e, "bob", "/bob_data/resource1", "GET", false) + testEnforce(t, e, "bob", "/bob_data/resource1", "POST", true) + testEnforce(t, e, "bob", "/bob_data/resource2", "GET", false) + testEnforce(t, e, "bob", "/bob_data/resource2", "POST", true) + + testEnforce(t, e, "cathy", "/cathy_data", "GET", true) + testEnforce(t, e, "cathy", "/cathy_data", "POST", true) + testEnforce(t, e, "cathy", "/cathy_data", "DELETE", false) + + e, _ = NewEnforcer(m) + _ = a.LoadPolicy(e.GetModel()) + + testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true) + testEnforce(t, e, "alice", "/alice_data/resource1", "POST", true) + testEnforce(t, e, "alice", "/alice_data/resource2", "GET", true) + testEnforce(t, e, "alice", "/alice_data/resource2", "POST", false) + testEnforce(t, e, "alice", "/bob_data/resource1", "GET", false) + testEnforce(t, e, "alice", "/bob_data/resource1", "POST", false) + testEnforce(t, e, "alice", "/bob_data/resource2", "GET", false) + testEnforce(t, e, "alice", "/bob_data/resource2", "POST", false) + + testEnforce(t, e, "bob", "/alice_data/resource1", "GET", false) + testEnforce(t, e, "bob", "/alice_data/resource1", "POST", false) + testEnforce(t, e, "bob", "/alice_data/resource2", "GET", true) + testEnforce(t, e, "bob", "/alice_data/resource2", "POST", false) + testEnforce(t, e, "bob", "/bob_data/resource1", "GET", false) + testEnforce(t, e, "bob", "/bob_data/resource1", "POST", true) + testEnforce(t, e, "bob", "/bob_data/resource2", "GET", false) + testEnforce(t, e, "bob", "/bob_data/resource2", "POST", true) + + testEnforce(t, e, "cathy", "/cathy_data", "GET", true) + testEnforce(t, e, "cathy", "/cathy_data", "POST", true) + testEnforce(t, e, "cathy", "/cathy_data", "DELETE", false) +} + +func TestKeyMatchWithRBACInDomain(t *testing.T) { + e, _ := NewEnforcer("examples/keymatch_with_rbac_in_domain.conf", "examples/keymatch_with_rbac_in_domain.csv") + testDomainEnforce(t, e, "Username==test2", "engines/engine1", "*", "attach", true) +} + +func TestKeyMatchModelInMemoryDeny(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("e", "e", "!some(where (p.eft == deny))") + m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)") + + a := fileadapter.NewAdapter("examples/keymatch_policy.csv") + + e, _ := NewEnforcer(m, a) + + testEnforce(t, e, "alice", "/alice_data/resource2", "POST", true) +} + +func TestRBACModelInMemoryIndeterminate(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("g", "g", "_, _") + m.AddDef("e", "e", "some(where (p.eft == allow))") + m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act") + + e, _ := NewEnforcer(m) + + _, _ = e.AddPermissionForUser("alice", "data1", "invalid") + + testEnforce(t, e, "alice", "data1", "read", false) +} + +func TestRBACModelInMemory(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("g", "g", "_, _") + m.AddDef("e", "e", "some(where (p.eft == allow))") + m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act") + + e, _ := NewEnforcer(m) + + _, _ = e.AddPermissionForUser("alice", "data1", "read") + _, _ = e.AddPermissionForUser("bob", "data2", "write") + _, _ = e.AddPermissionForUser("data2_admin", "data2", "read") + _, _ = e.AddPermissionForUser("data2_admin", "data2", "write") + _, _ = e.AddRoleForUser("alice", "data2_admin") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestRBACModelInMemory2(t *testing.T) { + text := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + m, _ := model.NewModelFromString(text) + // The above is the same as: + // m := NewModel() + // m.LoadModelFromText(text) + + e, _ := NewEnforcer(m) + + _, _ = e.AddPermissionForUser("alice", "data1", "read") + _, _ = e.AddPermissionForUser("bob", "data2", "write") + _, _ = e.AddPermissionForUser("data2_admin", "data2", "read") + _, _ = e.AddPermissionForUser("data2_admin", "data2", "write") + _, _ = e.AddRoleForUser("alice", "data2_admin") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestNotUsedRBACModelInMemory(t *testing.T) { + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("g", "g", "_, _") + m.AddDef("e", "e", "some(where (p.eft == allow))") + m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act") + + e, _ := NewEnforcer(m) + + _, _ = e.AddPermissionForUser("alice", "data1", "read") + _, _ = e.AddPermissionForUser("bob", "data2", "write") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestMatcherUsingInOperator(t *testing.T) { + // From file config + e, _ := NewEnforcer("examples/rbac_model_matcher_using_in_op.conf") + _, _ = e.AddPermissionForUser("alice", "data1", "read") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data3", "read", true) + testEnforce(t, e, "anyone", "data1", "read", false) + testEnforce(t, e, "anyone", "data2", "read", true) + testEnforce(t, e, "anyone", "data3", "read", true) +} + +func TestMatcherUsingInOperatorBracket(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model_matcher_using_in_op_bracket.conf") + _, _ = e.AddPermissionForUser("alice", "data1", "read") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data3", "read", true) + testEnforce(t, e, "anyone", "data1", "read", false) + testEnforce(t, e, "anyone", "data2", "read", true) + testEnforce(t, e, "anyone", "data3", "read", true) +} + +func TestReloadPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + _ = e.LoadPolicy() + testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) +} + +func TestSavePolicy(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + _ = e.SavePolicy() +} + +func TestClearPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + e.ClearPolicy() +} + +func TestEnableEnforce(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + e.EnableEnforce(false) + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", true) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", true) + testEnforce(t, e, "bob", "data1", "write", true) + testEnforce(t, e, "bob", "data2", "read", true) + testEnforce(t, e, "bob", "data2", "write", true) + + e.EnableEnforce(true) + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestEnableLog(t *testing.T) { + // This test is now a no-op since the logger has been removed + // Keeping it for backward compatibility, but it just tests enforcement + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestEnableAutoSave(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + e.EnableAutoSave(false) + // Because AutoSave is disabled, the policy change only affects the policy in Casbin enforcer, + // it doesn't affect the policy in the storage. + _, _ = e.RemovePolicy("alice", "data1", "read") + // Reload the policy from the storage to see the effect. + _ = e.LoadPolicy() + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) + + e.EnableAutoSave(true) + // Because AutoSave is enabled, the policy change not only affects the policy in Casbin enforcer, + // but also affects the policy in the storage. + _, _ = e.RemovePolicy("alice", "data1", "read") + + // However, the file adapter doesn't implement the AutoSave feature, so enabling it has no effect at all here. + + // Reload the policy from the storage to see the effect. + _ = e.LoadPolicy() + testEnforce(t, e, "alice", "data1", "read", true) // Will not be false here. + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestInitWithAdapter(t *testing.T) { + adapter := fileadapter.NewAdapter("examples/basic_policy.csv") + e, _ := NewEnforcer("examples/basic_model.conf", adapter) + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestRoleLinks(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf") + e.EnableAutoBuildRoleLinks(false) + _ = e.BuildRoleLinks() + _, _ = e.Enforce("user501", "data9", "read") +} + +func TestEnforceConcurrency(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Enforce is not concurrent") + } + }() + + e, _ := NewEnforcer("examples/rbac_model.conf") + _ = e.LoadModel() + + var wg sync.WaitGroup + + // Simulate concurrency (maybe use a timer?) + for i := 1; i <= 10000; i++ { + wg.Add(1) + go func() { + _, _ = e.Enforce("user501", "data9", "read") + wg.Done() + }() + } + + wg.Wait() +} + +func TestGetAndSetModel(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + e2, _ := NewEnforcer("examples/basic_with_root_model.conf", "examples/basic_policy.csv") + + testEnforce(t, e, "root", "data1", "read", false) + + e.SetModel(e2.GetModel()) + + testEnforce(t, e, "root", "data1", "read", true) +} + +func TestGetAndSetAdapterInMem(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + e2, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_inverse_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + + a2 := e2.GetAdapter() + e.SetAdapter(a2) + _ = e.LoadPolicy() + + testEnforce(t, e, "alice", "data1", "read", false) + testEnforce(t, e, "alice", "data1", "write", true) +} + +func TestSetAdapterFromFile(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf") + + testEnforce(t, e, "alice", "data1", "read", false) + + a := fileadapter.NewAdapter("examples/basic_policy.csv") + e.SetAdapter(a) + _ = e.LoadPolicy() + + testEnforce(t, e, "alice", "data1", "read", true) +} + +func TestInitEmpty(t *testing.T) { + e, _ := NewEnforcer() + + m := model.NewModel() + m.AddDef("r", "r", "sub, obj, act") + m.AddDef("p", "p", "sub, obj, act") + m.AddDef("e", "e", "some(where (p.eft == allow))") + m.AddDef("m", "m", "r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)") + + a := fileadapter.NewAdapter("examples/keymatch_policy.csv") + + e.SetModel(m) + e.SetAdapter(a) + _ = e.LoadPolicy() + + testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true) +} + +func testEnforceEx(t *testing.T, e *Enforcer, sub, obj, act interface{}, res []string) { + t.Helper() + _, myRes, _ := e.EnforceEx(sub, obj, act) + + if ok := util.ArrayEquals(res, myRes); !ok { + t.Error("Key: ", myRes, ", supposed to be ", res) + } +} + +func TestEnforceEx(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read"}) + testEnforceEx(t, e, "alice", "data1", "write", []string{}) + testEnforceEx(t, e, "alice", "data2", "read", []string{}) + testEnforceEx(t, e, "alice", "data2", "write", []string{}) + testEnforceEx(t, e, "bob", "data1", "read", []string{}) + testEnforceEx(t, e, "bob", "data1", "write", []string{}) + testEnforceEx(t, e, "bob", "data2", "read", []string{}) + testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write"}) + + e, _ = NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read"}) + testEnforceEx(t, e, "alice", "data1", "write", []string{}) + testEnforceEx(t, e, "alice", "data2", "read", []string{"data2_admin", "data2", "read"}) + testEnforceEx(t, e, "alice", "data2", "write", []string{"data2_admin", "data2", "write"}) + testEnforceEx(t, e, "bob", "data1", "read", []string{}) + testEnforceEx(t, e, "bob", "data1", "write", []string{}) + testEnforceEx(t, e, "bob", "data2", "read", []string{}) + testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write"}) + + e, _ = NewEnforcer("examples/priority_model.conf", "examples/priority_policy.csv") + + testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read", "allow"}) + testEnforceEx(t, e, "alice", "data1", "write", []string{"data1_deny_group", "data1", "write", "deny"}) + testEnforceEx(t, e, "alice", "data2", "read", []string{}) + testEnforceEx(t, e, "alice", "data2", "write", []string{}) + testEnforceEx(t, e, "bob", "data1", "write", []string{}) + testEnforceEx(t, e, "bob", "data2", "read", []string{"data2_allow_group", "data2", "read", "allow"}) + testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write", "deny"}) + + e, _ = NewEnforcer("examples/abac_model.conf") + obj := struct{ Owner string }{Owner: "alice"} + testEnforceEx(t, e, "alice", obj, "write", []string{}) +} + +func TestEnforceExLog(t *testing.T) { + // This test was previously named for logging, but actually tests EnforceEx explain functionality + // Logger parameter has been removed, but the test still validates explain behavior + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + testEnforceEx(t, e, "alice", "data1", "read", []string{"alice", "data1", "read"}) + testEnforceEx(t, e, "alice", "data1", "write", []string{}) + testEnforceEx(t, e, "alice", "data2", "read", []string{}) + testEnforceEx(t, e, "alice", "data2", "write", []string{}) + testEnforceEx(t, e, "bob", "data1", "read", []string{}) + testEnforceEx(t, e, "bob", "data1", "write", []string{}) + testEnforceEx(t, e, "bob", "data2", "read", []string{}) + testEnforceEx(t, e, "bob", "data2", "write", []string{"bob", "data2", "write"}) +} + +func testBatchEnforce(t *testing.T, e *Enforcer, requests [][]interface{}, results []bool) { + t.Helper() + myRes, _ := e.BatchEnforce(requests) + if len(myRes) != len(results) { + t.Errorf("%v supposed to be %v", myRes, results) + } + for i, v := range myRes { + if v != results[i] { + t.Errorf("%v supposed to be %v", myRes, results) + } + } +} + +func TestBatchEnforce(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + results := []bool{true, true, false} + testBatchEnforce(t, e, [][]interface{}{{"alice", "data1", "read"}, {"bob", "data2", "write"}, {"jack", "data3", "read"}}, results) +} + +func TestSubjectPriority(t *testing.T) { + e, _ := NewEnforcer("examples/subject_priority_model.conf", "examples/subject_priority_policy.csv") + testBatchEnforce(t, e, [][]interface{}{ + {"jane", "data1", "read"}, + {"alice", "data1", "read"}, + }, []bool{ + true, true, + }) +} + +func TestSubjectPriorityWithDomain(t *testing.T) { + e, _ := NewEnforcer("examples/subject_priority_model_with_domain.conf", "examples/subject_priority_policy_with_domain.csv") + testBatchEnforce(t, e, [][]interface{}{ + {"alice", "data1", "domain1", "write"}, + {"bob", "data2", "domain2", "write"}, + }, []bool{ + true, true, + }) +} + +func TestSubjectPriorityInFilter(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/subject_priority_policy_with_domain.csv") + _ = e.InitWithAdapter("examples/subject_priority_model_with_domain.conf", adapter) + if err := e.loadFilteredPolicy(&fileadapter.Filter{ + P: []string{"", "", "domain1"}, + }); err != nil { + t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) + } + + testBatchEnforce(t, e, [][]interface{}{ + {"alice", "data1", "domain1", "write"}, + {"admin", "data1", "domain1", "write"}, + }, []bool{ + true, false, + }) +} + +func TestMultiplePolicyDefinitions(t *testing.T) { + e, _ := NewEnforcer("examples/multiple_policy_definitions_model.conf", "examples/multiple_policy_definitions_policy.csv") + enforceContext := NewEnforceContext("2") + enforceContext.EType = "e" + testBatchEnforce(t, e, [][]interface{}{ + {"alice", "data2", "read"}, + {enforceContext, struct{ Age int }{Age: 70}, "/data1", "read"}, + {enforceContext, struct{ Age int }{Age: 30}, "/data1", "read"}, + }, []bool{ + true, false, true, + }) +} + +func TestPriorityExplicit(t *testing.T) { + e, _ := NewEnforcer("examples/priority_model_explicit.conf", "examples/priority_policy_explicit.csv") + testBatchEnforce(t, e, [][]interface{}{ + {"alice", "data1", "write"}, + {"alice", "data1", "read"}, + {"bob", "data2", "read"}, + {"bob", "data2", "write"}, + {"data1_deny_group", "data1", "read"}, + {"data1_deny_group", "data1", "write"}, + {"data2_allow_group", "data2", "read"}, + {"data2_allow_group", "data2", "write"}, + }, []bool{ + true, true, false, true, false, false, true, true, + }) + + _, err := e.AddPolicy("1", "bob", "data2", "write", "deny") + if err != nil { + t.Fatalf("Add Policy: %v", err) + } + + testBatchEnforce(t, e, [][]interface{}{ + {"alice", "data1", "write"}, + {"alice", "data1", "read"}, + {"bob", "data2", "read"}, + {"bob", "data2", "write"}, + {"data1_deny_group", "data1", "read"}, + {"data1_deny_group", "data1", "write"}, + {"data2_allow_group", "data2", "read"}, + {"data2_allow_group", "data2", "write"}, + }, []bool{ + true, true, false, false, false, false, true, true, + }) +} + +func TestFailedToLoadPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv") + e.AddNamedMatchingFunc("g2", "matchingFunc", util.KeyMatch2) + testEnforce(t, e, "alice", "/book/1", "GET", true) + testEnforce(t, e, "bob", "/pen/3", "GET", true) + e.SetAdapter(fileadapter.NewAdapter("not found")) + _ = e.LoadPolicy() + testEnforce(t, e, "alice", "/book/1", "GET", true) + testEnforce(t, e, "bob", "/pen/3", "GET", true) +} + +func TestReloadPolicyWithFunc(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv") + e.AddNamedMatchingFunc("g2", "matchingFunc", util.KeyMatch2) + testEnforce(t, e, "alice", "/book/1", "GET", true) + testEnforce(t, e, "bob", "/pen/3", "GET", true) + _ = e.LoadPolicy() + testEnforce(t, e, "alice", "/book/1", "GET", true) + testEnforce(t, e, "bob", "/pen/3", "GET", true) +} + +func TestEvalPriority(t *testing.T) { + e, _ := NewEnforcer("examples/eval_operator_model.conf", "examples/eval_operator_policy.csv") + testEnforce(t, e, "admin", "users", "write", true) + testEnforce(t, e, "admin", "none", "write", false) + testEnforce(t, e, "user", "users", "write", false) +} + +func TestLinkConditionFunc(t *testing.T) { + TrueFunc := func(args ...string) (bool, error) { + if len(args) != 0 { + return args[0] == "_" || args[0] == "true", nil + } + return false, nil + } + + FalseFunc := func(args ...string) (bool, error) { + if len(args) != 0 { + return args[0] == "_" || args[0] == "false", nil + } + return false, nil + } + + m, _ := model.NewModelFromFile("examples/rbac_with_temporal_roles_model.conf") + e, _ := NewEnforcer(m) + + _, _ = e.AddPolicies([][]string{ + {"alice", "data1", "read"}, + {"alice", "data1", "write"}, + {"data2_admin", "data2", "read"}, + {"data2_admin", "data2", "write"}, + {"data3_admin", "data3", "read"}, + {"data3_admin", "data3", "write"}, + {"data4_admin", "data4", "read"}, + {"data4_admin", "data4", "write"}, + {"data5_admin", "data5", "read"}, + {"data5_admin", "data5", "write"}, + }) + + _, _ = e.AddGroupingPolicies([][]string{ + {"alice", "data2_admin", "_", "_"}, + {"alice", "data3_admin", "_", "_"}, + {"alice", "data4_admin", "_", "_"}, + {"alice", "data5_admin", "_", "_"}, + }) + + e.AddNamedLinkConditionFunc("g", "alice", "data2_admin", TrueFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data3_admin", TrueFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data4_admin", FalseFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data5_admin", FalseFunc) + + e.SetNamedLinkConditionFuncParams("g", "alice", "data2_admin", "true") + e.SetNamedLinkConditionFuncParams("g", "alice", "data3_admin", "not true") + e.SetNamedLinkConditionFuncParams("g", "alice", "data4_admin", "false") + e.SetNamedLinkConditionFuncParams("g", "alice", "data5_admin", "not false") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", true) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "alice", "data3", "read", false) + testEnforce(t, e, "alice", "data3", "write", false) + testEnforce(t, e, "alice", "data4", "read", true) + testEnforce(t, e, "alice", "data4", "write", true) + testEnforce(t, e, "alice", "data5", "read", false) + testEnforce(t, e, "alice", "data5", "write", false) + + m, _ = model.NewModelFromFile("examples/rbac_with_domain_temporal_roles_model.conf") + e, _ = NewEnforcer(m) + + _, _ = e.AddPolicies([][]string{ + {"alice", "domain1", "data1", "read"}, + {"alice", "domain1", "data1", "write"}, + {"data2_admin", "domain2", "data2", "read"}, + {"data2_admin", "domain2", "data2", "write"}, + {"data3_admin", "domain3", "data3", "read"}, + {"data3_admin", "domain3", "data3", "write"}, + {"data4_admin", "domain4", "data4", "read"}, + {"data4_admin", "domain4", "data4", "write"}, + {"data5_admin", "domain5", "data5", "read"}, + {"data5_admin", "domain5", "data5", "write"}, + }) + + _, _ = e.AddGroupingPolicies([][]string{ + {"alice", "data2_admin", "domain2", "_", "_"}, + {"alice", "data3_admin", "domain3", "_", "_"}, + {"alice", "data4_admin", "domain4", "_", "_"}, + {"alice", "data5_admin", "domain5", "_", "_"}, + }) + + e.AddNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", TrueFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data3_admin", "domain3", TrueFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data4_admin", "domain4", FalseFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data5_admin", "domain5", FalseFunc) + + e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data2_admin", "domain2", "true") + e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data3_admin", "domain3", "not true") + e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data4_admin", "domain4", "false") + e.SetNamedDomainLinkConditionFuncParams("g", "alice", "data5_admin", "domain5", "not false") + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true) + testDomainEnforce(t, e, "alice", "domain2", "data2", "read", true) + testDomainEnforce(t, e, "alice", "domain2", "data2", "write", true) + testDomainEnforce(t, e, "alice", "domain3", "data3", "read", false) + testDomainEnforce(t, e, "alice", "domain3", "data3", "write", false) + testDomainEnforce(t, e, "alice", "domain4", "data4", "read", true) + testDomainEnforce(t, e, "alice", "domain4", "data4", "write", true) + testDomainEnforce(t, e, "alice", "domain5", "data5", "read", false) + testDomainEnforce(t, e, "alice", "domain5", "data5", "write", false) +} + +func TestEnforcerWithDefaultDetector(t *testing.T) { + // Test that default detector is enabled and detects cycles + _, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_with_cycle_policy.csv") + + // Expect an error because the policy contains a cycle + if err == nil { + t.Error("Expected cycle detection error when loading policy with cycle, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'cycle detected', got: %s", errMsg) + } + } +} + +func TestEnforcerRunDetections(t *testing.T) { + // Test explicit RunDetections() call + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + // Should not error on valid policy + err := e.RunDetections() + if err != nil { + t.Errorf("Expected no error when running detections on valid policy, but got: %v", err) + } + + // Now add a cycle manually + _, _ = e.AddGroupingPolicy("alice", "data2_admin") + _, _ = e.AddGroupingPolicy("data2_admin", "super_admin") + _, _ = e.AddGroupingPolicy("super_admin", "alice") + + // Should detect the cycle + err = e.RunDetections() + if err == nil { + t.Error("Expected cycle detection error, but got nil") + } else { + errMsg := err.Error() + if !strings.Contains(errMsg, "cycle detected") { + t.Errorf("Expected error message to contain 'cycle detected', got: %s", errMsg) + } + } +} + +func TestEnforcerSetDetector(t *testing.T) { + // Test SetDetector() method + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + // Create a custom detector + customDetector := detector.NewDefaultDetector() + e.SetDetector(customDetector) + + // Should still work with custom detector + err := e.RunDetections() + if err != nil { + t.Errorf("Expected no error with custom detector, but got: %v", err) + } +} + +func TestEnforcerSetDetectors(t *testing.T) { + // Test SetDetectors() method + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + // Create multiple detectors + detectors := []detector.Detector{ + detector.NewDefaultDetector(), + detector.NewDefaultDetector(), + } + e.SetDetectors(detectors) + + // Should work with multiple detectors + err := e.RunDetections() + if err != nil { + t.Errorf("Expected no error with multiple detectors, but got: %v", err) + } +} diff --git a/enforcer_transactional.go b/enforcer_transactional.go new file mode 100644 index 0000000..0222281 --- /dev/null +++ b/enforcer_transactional.go @@ -0,0 +1,124 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "time" + + "github.com/casbin/casbin/v3/persist" + "github.com/google/uuid" +) + +// TransactionalEnforcer extends Enforcer with transaction support. +// It provides atomic policy operations through transactions. +type TransactionalEnforcer struct { + *Enforcer // Embedded enforcer for all standard functionality + activeTransactions sync.Map // Stores active transactions. + modelVersion int64 // Model version number for optimistic locking. + commitLock sync.Mutex // Protects commit and rollback operations. +} + +// NewTransactionalEnforcer creates a new TransactionalEnforcer. +// It accepts the same parameters as NewEnforcer. +func NewTransactionalEnforcer(params ...interface{}) (*TransactionalEnforcer, error) { + enforcer, err := NewEnforcer(params...) + if err != nil { + return nil, err + } + + return &TransactionalEnforcer{ + Enforcer: enforcer, + }, nil +} + +// BeginTransaction starts a new transaction. +// Returns an error if a transaction is already in progress or if the adapter doesn't support transactions. +func (te *TransactionalEnforcer) BeginTransaction(ctx context.Context) (*Transaction, error) { + // Check if adapter supports transactions. + txAdapter, ok := te.adapter.(persist.TransactionalAdapter) + if !ok { + return nil, errors.New("adapter does not support transactions") + } + + // Start database transaction. + txContext, err := txAdapter.BeginTransaction(ctx) + if err != nil { + return nil, err + } + + // Create transaction buffer with current model snapshot. + buffer := NewTransactionBuffer(te.model) + + tx := &Transaction{ + id: uuid.New().String(), + enforcer: te, + buffer: buffer, + txContext: txContext, + ctx: ctx, + baseVersion: atomic.LoadInt64(&te.modelVersion), + startTime: time.Now(), + } + + te.activeTransactions.Store(tx.id, tx) + return tx, nil +} + +// GetTransaction returns a transaction by its ID, or nil if not found. +func (te *TransactionalEnforcer) GetTransaction(id string) *Transaction { + if tx, ok := te.activeTransactions.Load(id); ok { + return tx.(*Transaction) + } + return nil +} + +// IsTransactionActive returns true if the transaction with the given ID is active. +func (te *TransactionalEnforcer) IsTransactionActive(id string) bool { + if tx := te.GetTransaction(id); tx != nil { + return tx.IsActive() + } + return false +} + +// WithTransaction executes a function within a transaction. +// If the function returns an error, the transaction is rolled back. +// Otherwise, it's committed automatically. +func (te *TransactionalEnforcer) WithTransaction(ctx context.Context, fn func(*Transaction) error) error { + tx, err := te.BeginTransaction(ctx) + if err != nil { + return err + } + + defer func() { + if r := recover(); r != nil { + _ = tx.Rollback() + panic(r) + } + }() + + err = fn(tx) + if err != nil { + _ = tx.Rollback() + return err + } + + return tx.Commit() +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..194488a --- /dev/null +++ b/error_test.go @@ -0,0 +1,288 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" + + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" +) + +func TestPathError(t *testing.T) { + _, err := NewEnforcer("hope_this_path_wont_exist", "") + if err == nil { + t.Errorf("Should be error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } +} + +func TestEnforcerParamError(t *testing.T) { + _, err := NewEnforcer(1, 2, 3) + if err == nil { + t.Errorf("Should not be error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } + + _, err2 := NewEnforcer(1, "2") + if err2 == nil { + t.Errorf("Should not be error here.") + } else { + t.Log("Test on error: ") + t.Log(err2.Error()) + } +} + +func TestModelError(t *testing.T) { + _, err := NewEnforcer("examples/error/error_model.conf", "examples/error/error_policy.csv") + if err == nil { + t.Errorf("Should be error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } +} + +// func TestPolicyError(t *testing.T) { +// _, err := NewEnforcer("examples/basic_model.conf", "examples/error/error_policy.csv") +// if err == nil { +// t.Errorf("Should be error here.") +// } else { +// t.Log("Test on error: ") +// t.Log(err.Error()) +// } +//} + +func TestEnforceError(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + _, err := e.Enforce("wrong", "wrong") + if err == nil { + t.Errorf("Should be error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } + + e, _ = NewEnforcer("examples/abac_rule_model.conf") + _, err = e.Enforce("wang", "wang", "wang") + if err == nil { + t.Errorf("Should be error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } +} + +func TestNoError(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + err := e.LoadModel() + if err != nil { + t.Errorf("Should be no error here.") + t.Log("Unexpected error: ") + t.Log(err.Error()) + } + + err = e.LoadPolicy() + if err != nil { + t.Errorf("Should be no error here.") + t.Log("Unexpected error: ") + t.Log(err.Error()) + } + + err = e.SavePolicy() + if err != nil { + t.Errorf("Should be no error here.") + t.Log("Unexpected error: ") + t.Log(err.Error()) + } +} + +func TestModelNoError(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + e.modelPath = "hope_this_path_wont_exist" + err := e.LoadModel() + + if err == nil { + t.Errorf("Should be error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } +} + +func TestMockAdapterErrors(t *testing.T) { + adapter := fileadapter.NewAdapterMock("examples/rbac_with_domains_policy.csv") + adapter.SetMockErr("mock error") + + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", adapter) + + added, err := e.AddPolicy("admin", "domain3", "data1", "read") + if added { + t.Errorf("added should be false") + } + + if err == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } + + rules := [][]string{ + {"admin", "domain4", "data1", "read"}, + } + added, err = e.AddPolicies(rules) + if added { + t.Errorf("added should be false") + } + + if err == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } + + removed, err2 := e.RemoveFilteredPolicy(1, "domain1", "data1") + if removed { + t.Errorf("removed should be false") + } + + if err2 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err2.Error()) + } + + removed, err3 := e.RemovePolicy("admin", "domain2", "data2", "read") + if removed { + t.Errorf("removed should be false") + } + + if err3 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err3.Error()) + } + + rules = [][]string{ + {"admin", "domain1", "data1", "read"}, + } + removed, err = e.RemovePolicies(rules) + if removed { + t.Errorf("removed should be false") + } + + if err == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } + + added, err4 := e.AddGroupingPolicy("bob", "admin2") + if added { + t.Errorf("added should be false") + } + + if err4 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err4.Error()) + } + + added, err5 := e.AddNamedGroupingPolicy("g", []string{"eve", "admin2", "domain1"}) + if added { + t.Errorf("added should be false") + } + + if err5 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err5.Error()) + } + + added, err6 := e.AddNamedPolicy("p", []string{"admin2", "domain2", "data2", "write"}) + if added { + t.Errorf("added should be false") + } + + if err6 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err6.Error()) + } + + removed, err7 := e.RemoveGroupingPolicy("bob", "admin2") + if removed { + t.Errorf("removed should be false") + } + + if err7 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err7.Error()) + } + + removed, err8 := e.RemoveFilteredGroupingPolicy(0, "bob") + if removed { + t.Errorf("removed should be false") + } + + if err8 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err8.Error()) + } + + removed, err9 := e.RemoveNamedGroupingPolicy("g", []string{"alice", "admin", "domain1"}) + if removed { + t.Errorf("removed should be false") + } + + if err9 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err9.Error()) + } + + removed, err10 := e.RemoveFilteredNamedGroupingPolicy("g", 0, "eve") + if removed { + t.Errorf("removed should be false") + } + + if err10 == nil { + t.Errorf("Should be an error here.") + } else { + t.Log("Test on error: ") + t.Log(err10.Error()) + } +} diff --git a/errors/constraint_errors.go b/errors/constraint_errors.go new file mode 100644 index 0000000..40460c3 --- /dev/null +++ b/errors/constraint_errors.go @@ -0,0 +1,49 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package errors + +import ( + "errors" + "fmt" +) + +// Global errors for constraints defined here. +var ( + ErrConstraintViolation = errors.New("constraint violation") + ErrConstraintParsingError = errors.New("constraint parsing error") + ErrConstraintRequiresRBAC = errors.New("constraints require RBAC to be enabled (role_definition section must exist)") + ErrInvalidConstraintDefinition = errors.New("invalid constraint definition") +) + +// ConstraintViolationError represents a specific constraint violation. +type ConstraintViolationError struct { + ConstraintName string + Message string +} + +func (e *ConstraintViolationError) Error() string { + return fmt.Sprintf("constraint violation [%s]: %s", e.ConstraintName, e.Message) +} + +// NewConstraintViolationError creates a new constraint violation error. +func NewConstraintViolationError(constraintName, message string) error { + return &ConstraintViolationError{ + ConstraintName: constraintName, + Message: message, + } +} diff --git a/errors/rbac_errors.go b/errors/rbac_errors.go new file mode 100644 index 0000000..176df9c --- /dev/null +++ b/errors/rbac_errors.go @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package errors + +import "errors" + +// Global errors for rbac defined here. +var ( + ErrNameNotFound = errors.New("error: name does not exist") + ErrDomainParameter = errors.New("error: domain should be 1 parameter") + ErrLinkNotFound = errors.New("error: link between name1 and name2 does not exist") + ErrUseDomainParameter = errors.New("error: useDomain should be 1 parameter") + ErrInvalidFieldValuesParameter = errors.New("fieldValues requires at least one parameter") + + // GetAllowedObjectConditions errors. + ErrObjCondition = errors.New("need to meet the prefix required by the object condition") + ErrEmptyCondition = errors.New("GetAllowedObjectConditions have an empty condition") +) diff --git a/examples/abac_model.conf b/examples/abac_model.conf new file mode 100644 index 0000000..4cfaea3 --- /dev/null +++ b/examples/abac_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == r.obj.Owner \ No newline at end of file diff --git a/examples/abac_not_using_policy_model.conf b/examples/abac_not_using_policy_model.conf new file mode 100644 index 0000000..64c708a --- /dev/null +++ b/examples/abac_not_using_policy_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[policy_effect] +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[matchers] +m = r.sub == r.obj.Owner \ No newline at end of file diff --git a/examples/abac_rule_effect_policy.csv b/examples/abac_rule_effect_policy.csv new file mode 100644 index 0000000..bea9962 --- /dev/null +++ b/examples/abac_rule_effect_policy.csv @@ -0,0 +1,4 @@ +p, alice, /data1, read, deny +p, alice, /data1, write, allow +p, bob, /data2, write, deny +p, bob, /data2, read, allow \ No newline at end of file diff --git a/examples/abac_rule_model.conf b/examples/abac_rule_model.conf new file mode 100644 index 0000000..591dd3a --- /dev/null +++ b/examples/abac_rule_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub_rule, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = eval(p.sub_rule) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/abac_rule_policy.csv b/examples/abac_rule_policy.csv new file mode 100644 index 0000000..e3dbc83 --- /dev/null +++ b/examples/abac_rule_policy.csv @@ -0,0 +1,2 @@ +p, r.sub.Age > 18, /data1, read +p, r.sub.Age < 60, /data2, write \ No newline at end of file diff --git a/examples/basic_inverse_policy.csv b/examples/basic_inverse_policy.csv new file mode 100644 index 0000000..276c440 --- /dev/null +++ b/examples/basic_inverse_policy.csv @@ -0,0 +1,2 @@ +p, alice, data1, write +p, bob, data2, read \ No newline at end of file diff --git a/examples/basic_model.conf b/examples/basic_model.conf new file mode 100644 index 0000000..dc6da81 --- /dev/null +++ b/examples/basic_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/basic_model_without_spaces.conf b/examples/basic_model_without_spaces.conf new file mode 100644 index 0000000..5452f95 --- /dev/null +++ b/examples/basic_model_without_spaces.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub,obj,act + +[policy_definition] +p = sub,obj,act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/basic_policy.csv b/examples/basic_policy.csv new file mode 100644 index 0000000..57aaa97 --- /dev/null +++ b/examples/basic_policy.csv @@ -0,0 +1,2 @@ +p, alice, data1, read +p, bob, data2, write \ No newline at end of file diff --git a/examples/basic_with_root_model.conf b/examples/basic_with_root_model.conf new file mode 100644 index 0000000..8f13907 --- /dev/null +++ b/examples/basic_with_root_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root" \ No newline at end of file diff --git a/examples/basic_without_resources_model.conf b/examples/basic_without_resources_model.conf new file mode 100644 index 0000000..f61bd71 --- /dev/null +++ b/examples/basic_without_resources_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, act + +[policy_definition] +p = sub, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.act == p.act \ No newline at end of file diff --git a/examples/basic_without_resources_policy.csv b/examples/basic_without_resources_policy.csv new file mode 100644 index 0000000..c861941 --- /dev/null +++ b/examples/basic_without_resources_policy.csv @@ -0,0 +1,2 @@ +p, alice, read +p, bob, write \ No newline at end of file diff --git a/examples/basic_without_users_model.conf b/examples/basic_without_users_model.conf new file mode 100644 index 0000000..1fe5993 --- /dev/null +++ b/examples/basic_without_users_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = obj, act + +[policy_definition] +p = obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/basic_without_users_policy.csv b/examples/basic_without_users_policy.csv new file mode 100644 index 0000000..79048da --- /dev/null +++ b/examples/basic_without_users_policy.csv @@ -0,0 +1,2 @@ +p, data1, read +p, data2, write \ No newline at end of file diff --git a/examples/biba_model.conf b/examples/biba_model.conf new file mode 100644 index 0000000..bfc03de --- /dev/null +++ b/examples/biba_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, sub_level, obj, obj_level, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m =(r.act == "read" && r.sub_level <= r.obj_level) || (r.act == "write" && r.sub_level >= r.obj_level) \ No newline at end of file diff --git a/examples/blp_model.conf b/examples/blp_model.conf new file mode 100644 index 0000000..3ee9fb2 --- /dev/null +++ b/examples/blp_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, sub_level, obj, obj_level, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = (r.act == "read" && r.sub_level >= r.obj_level) || (r.act == "write" && r.sub_level <= r.obj_level) \ No newline at end of file diff --git a/examples/comment_model.conf b/examples/comment_model.conf new file mode 100644 index 0000000..a4200eb --- /dev/null +++ b/examples/comment_model.conf @@ -0,0 +1,12 @@ +[request_definition] +r = sub, obj, act ; Request definition + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) # This is policy effect. + +# Matchers +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/error/error_model.conf b/examples/error/error_model.conf new file mode 100644 index 0000000..59304db --- /dev/null +++ b/examples/error/error_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/error/error_policy.csv b/examples/error/error_policy.csv new file mode 100644 index 0000000..e490c7d --- /dev/null +++ b/examples/error/error_policy.csv @@ -0,0 +1,2 @@ +p, alice, data1, read +bob, data2, write \ No newline at end of file diff --git a/examples/eval_operator_model.conf b/examples/eval_operator_model.conf new file mode 100644 index 0000000..9ffb5ba --- /dev/null +++ b/examples/eval_operator_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub_rule, obj_rule, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = eval(p.sub_rule) && eval(p.obj_rule) && (p.act == '*' || r.act == p.act) diff --git a/examples/eval_operator_policy.csv b/examples/eval_operator_policy.csv new file mode 100644 index 0000000..85665a8 --- /dev/null +++ b/examples/eval_operator_policy.csv @@ -0,0 +1 @@ +p, r.sub == 'admin' || false, r.obj == 'users', write \ No newline at end of file diff --git a/examples/glob_model.conf b/examples/glob_model.conf new file mode 100644 index 0000000..b16cad4 --- /dev/null +++ b/examples/glob_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && globMatch(r.obj, p.obj) && r.act == p.act \ No newline at end of file diff --git a/examples/glob_policy.csv b/examples/glob_policy.csv new file mode 100644 index 0000000..86c03b0 --- /dev/null +++ b/examples/glob_policy.csv @@ -0,0 +1,4 @@ +p, u1, /foo/*, read +p, u2, /foo*, read +p, u3, /*/foo/*, read +p, u4, *, read \ No newline at end of file diff --git a/examples/ipmatch_model.conf b/examples/ipmatch_model.conf new file mode 100644 index 0000000..26e4b01 --- /dev/null +++ b/examples/ipmatch_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = ipMatch(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/ipmatch_policy.csv b/examples/ipmatch_policy.csv new file mode 100644 index 0000000..ca678a9 --- /dev/null +++ b/examples/ipmatch_policy.csv @@ -0,0 +1,2 @@ +p, 192.168.2.0/24, data1, read +p, 10.0.0.0/16, data2, write \ No newline at end of file diff --git a/examples/keyget2_model.conf b/examples/keyget2_model.conf new file mode 100644 index 0000000..5b569a6 --- /dev/null +++ b/examples/keyget2_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyGet2(r.obj, p.obj, 'resource') in ('age', 'name') && regexMatch(r.act, p.act) diff --git a/examples/keyget_model.conf b/examples/keyget_model.conf new file mode 100644 index 0000000..cc92832 --- /dev/null +++ b/examples/keyget_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && (r.obj == p.obj || keyGet(r.obj, p.obj) in ('age','name')) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/keymatch2_model.conf b/examples/keymatch2_model.conf new file mode 100644 index 0000000..944123d --- /dev/null +++ b/examples/keymatch2_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/keymatch2_policy.csv b/examples/keymatch2_policy.csv new file mode 100644 index 0000000..941a48f --- /dev/null +++ b/examples/keymatch2_policy.csv @@ -0,0 +1,2 @@ +p, alice, /alice_data/:resource, GET +p, alice, /alice_data2/:id/using/:resId, GET \ No newline at end of file diff --git a/examples/keymatch_custom_model.conf b/examples/keymatch_custom_model.conf new file mode 100644 index 0000000..1cad8bf --- /dev/null +++ b/examples/keymatch_custom_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatchCustom(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/keymatch_model.conf b/examples/keymatch_model.conf new file mode 100644 index 0000000..4f86ba8 --- /dev/null +++ b/examples/keymatch_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/keymatch_policy.csv b/examples/keymatch_policy.csv new file mode 100644 index 0000000..d6e9b7d --- /dev/null +++ b/examples/keymatch_policy.csv @@ -0,0 +1,7 @@ +p, alice, /alice_data/*, GET +p, alice, /alice_data/resource1, POST + +p, bob, /alice_data/resource2, GET +p, bob, /bob_data/*, POST + +p, cathy, /cathy_data, (GET)|(POST) \ No newline at end of file diff --git a/examples/keymatch_with_rbac_in_domain.conf b/examples/keymatch_with_rbac_in_domain.conf new file mode 100644 index 0000000..396fb45 --- /dev/null +++ b/examples/keymatch_with_rbac_in_domain.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && keyMatch(r.dom, p.dom) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) diff --git a/examples/keymatch_with_rbac_in_domain.csv b/examples/keymatch_with_rbac_in_domain.csv new file mode 100644 index 0000000..300579f --- /dev/null +++ b/examples/keymatch_with_rbac_in_domain.csv @@ -0,0 +1,6 @@ +g, can_manage, can_use, * + +p, can_manage, engines/*, *, (pause)|(resume) +p, can_use, engines/*, *, (attach)|(detach) + +g, Username==test2, can_manage, engines/engine1 diff --git a/examples/lbac_model.conf b/examples/lbac_model.conf new file mode 100644 index 0000000..be509a2 --- /dev/null +++ b/examples/lbac_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, subject_confidentiality, subject_integrity, obj, object_confidentiality, object_integrity, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = (r.act == "read" && r.subject_confidentiality >= r.object_confidentiality && r.subject_integrity >= r.object_integrity) || (r.act == "write" && r.subject_confidentiality <= r.object_confidentiality && r.subject_integrity <= r.object_integrity) \ No newline at end of file diff --git a/examples/multiple_policy_definitions_model.conf b/examples/multiple_policy_definitions_model.conf new file mode 100644 index 0000000..b619097 --- /dev/null +++ b/examples/multiple_policy_definitions_model.conf @@ -0,0 +1,19 @@ +[request_definition] +r = sub, obj, act +r2 = sub, obj, act + +[policy_definition] +p = sub, obj, act +p2= sub_rule, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +#RABC +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +#ABAC +m2 = eval(p2.sub_rule) && r2.obj == p2.obj && r2.act == p2.act diff --git a/examples/multiple_policy_definitions_policy.csv b/examples/multiple_policy_definitions_policy.csv new file mode 100644 index 0000000..66498ab --- /dev/null +++ b/examples/multiple_policy_definitions_policy.csv @@ -0,0 +1,5 @@ +p, data2_admin, data2, read +p2, r2.sub.Age > 18 && r2.sub.Age < 60, /data1, read, allow +p2, r2.sub.Age > 60 && r2.sub.Age < 100, /data1, read, deny + +g, alice, data2_admin \ No newline at end of file diff --git a/examples/object_conditions_model.conf b/examples/object_conditions_model.conf new file mode 100644 index 0000000..55aa185 --- /dev/null +++ b/examples/object_conditions_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, sub_rule, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && eval(p.sub_rule) && r.act == p.act \ No newline at end of file diff --git a/examples/object_conditions_policy.csv b/examples/object_conditions_policy.csv new file mode 100644 index 0000000..7dad5c8 --- /dev/null +++ b/examples/object_conditions_policy.csv @@ -0,0 +1,5 @@ +p, alice, r.obj.price < 25, read +p, admin, r.obj.category_id = 2, read +p, bob, r.obj.author = bob, write + +g, alice, admin \ No newline at end of file diff --git a/examples/orbac_model.conf b/examples/orbac_model.conf new file mode 100644 index 0000000..ed2f4ee --- /dev/null +++ b/examples/orbac_model.conf @@ -0,0 +1,16 @@ +[request_definition] +r = sub, org, obj, act + +[policy_definition] +p = role, activity, view, org + +[role_definition] +g = _, _, _ +g2 = _, _, _ +g3 = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.role, r.org) && g2(r.act, p.activity, r.org) && g3(r.obj, p.view, r.org) && r.org == p.org diff --git a/examples/orbac_policy.csv b/examples/orbac_policy.csv new file mode 100644 index 0000000..91c3b4b --- /dev/null +++ b/examples/orbac_policy.csv @@ -0,0 +1,25 @@ +# Permission rules: role, activity, view, organization +p, manager, modify, document, org1 +p, manager, consult, document, org1 +p, employee, consult, document, org1 +p, manager, modify, report, org2 +p, manager, consult, report, org2 +p, employee, consult, report, org2 + +# Empower: subject, role, organization +g, alice, manager, org1 +g, bob, employee, org1 +g, charlie, manager, org2 +g, david, employee, org2 + +# Use: action, activity, organization +g2, write, modify, org1 +g2, read, consult, org1 +g2, write, modify, org2 +g2, read, consult, org2 + +# Consider: object, view, organization +g3, data1, document, org1 +g3, data2, document, org1 +g3, report1, report, org2 +g3, report2, report, org2 diff --git a/examples/pbac_model.conf b/examples/pbac_model.conf new file mode 100644 index 0000000..23023bd --- /dev/null +++ b/examples/pbac_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub_rule, obj_rule, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = eval(p.sub_rule) && eval(p.obj_rule) && r.act == p.act \ No newline at end of file diff --git a/examples/pbac_policy.csv b/examples/pbac_policy.csv new file mode 100644 index 0000000..23a76f1 --- /dev/null +++ b/examples/pbac_policy.csv @@ -0,0 +1,2 @@ +p, r.sub.Role == 'admin', r.obj.Type == 'doc', read +p, r.sub.Age >= 18, r.obj.Type == 'video', play diff --git a/examples/performance/rbac_with_pattern_large_scale_model.conf b/examples/performance/rbac_with_pattern_large_scale_model.conf new file mode 100644 index 0000000..19c4052 --- /dev/null +++ b/examples/performance/rbac_with_pattern_large_scale_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.obj) && keyMatch4(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/priority_indeterminate_policy.csv b/examples/priority_indeterminate_policy.csv new file mode 100644 index 0000000..974aa27 --- /dev/null +++ b/examples/priority_indeterminate_policy.csv @@ -0,0 +1 @@ +p, alice, data1, read, indeterminate \ No newline at end of file diff --git a/examples/priority_model.conf b/examples/priority_model.conf new file mode 100644 index 0000000..ece1562 --- /dev/null +++ b/examples/priority_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = priority(p.eft) || deny + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/priority_model_enforce_context.conf b/examples/priority_model_enforce_context.conf new file mode 100644 index 0000000..662aeb8 --- /dev/null +++ b/examples/priority_model_enforce_context.conf @@ -0,0 +1,16 @@ +[request_definition] +r = sub, obj, act +r2 = sub, obj + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = priority(p.eft) || deny + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +m2 = g(r2.sub, p.sub) diff --git a/examples/priority_model_explicit.conf b/examples/priority_model_explicit.conf new file mode 100644 index 0000000..5df75b2 --- /dev/null +++ b/examples/priority_model_explicit.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = priority, sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = priority(p.eft) || deny + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/priority_model_explicit_customized.conf b/examples/priority_model_explicit_customized.conf new file mode 100644 index 0000000..5071fa7 --- /dev/null +++ b/examples/priority_model_explicit_customized.conf @@ -0,0 +1,14 @@ +[request_definition] +r = subject, obj, act + +[policy_definition] +p = customized_priority, obj, act, eft, subject + +[role_definition] +g = _, _ + +[policy_effect] +e = priority(p.eft) || deny + +[matchers] +m = g(r.subject, p.subject) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/priority_policy.csv b/examples/priority_policy.csv new file mode 100644 index 0000000..1ec5e5a --- /dev/null +++ b/examples/priority_policy.csv @@ -0,0 +1,12 @@ +p, alice, data1, read, allow +p, data1_deny_group, data1, read, deny +p, data1_deny_group, data1, write, deny +p, alice, data1, write, allow + +g, alice, data1_deny_group + +p, data2_allow_group, data2, read, allow +p, bob, data2, read, deny +p, bob, data2, write, deny + +g, bob, data2_allow_group \ No newline at end of file diff --git a/examples/priority_policy_enforce_context.csv b/examples/priority_policy_enforce_context.csv new file mode 100644 index 0000000..756de5c --- /dev/null +++ b/examples/priority_policy_enforce_context.csv @@ -0,0 +1,12 @@ +p, alice, data1, read, allow +p, data1_deny_group, data1, read, deny +p, data1_deny_group, data1, write, deny +p, alice, data1, write, allow + +g, alice, data1_deny_group + +p, data2_allow_group, data2, read, allow +p, bob, data2, read, deny +p, bob, data2, write, deny + +g, bob, data2_allow_group diff --git a/examples/priority_policy_explicit.csv b/examples/priority_policy_explicit.csv new file mode 100644 index 0000000..0fec82c --- /dev/null +++ b/examples/priority_policy_explicit.csv @@ -0,0 +1,12 @@ +p, 10, data1_deny_group, data1, read, deny +p, 10, data1_deny_group, data1, write, deny +p, 10, data2_allow_group, data2, read, allow +p, 10, data2_allow_group, data2, write, allow + + +p, 1, alice, data1, write, allow +p, 1, alice, data1, read, allow +p, 1, bob, data2, read, deny + +g, bob, data2_allow_group +g, alice, data1_deny_group diff --git a/examples/priority_policy_explicit_customized.csv b/examples/priority_policy_explicit_customized.csv new file mode 100644 index 0000000..a861e2b --- /dev/null +++ b/examples/priority_policy_explicit_customized.csv @@ -0,0 +1,12 @@ +p, 10, data1, read, deny, data1_deny_group +p, 10, data1, write, deny, data1_deny_group +p, 10, data2, read, allow, data2_allow_group +p, 10, data2, write, allow, data2_allow_group + + +p, 1, data1, write, allow, alice +p, 1, data1, read, allow, alice +p, 1, data2, read, deny, bob + +g, bob, data2_allow_group +g, alice, data1_deny_group diff --git a/examples/rbac_model.conf b/examples/rbac_model.conf new file mode 100644 index 0000000..71159e3 --- /dev/null +++ b/examples/rbac_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_model_in_multi_line.conf b/examples/rbac_model_in_multi_line.conf new file mode 100644 index 0000000..17771b6 --- /dev/null +++ b/examples/rbac_model_in_multi_line.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj \ + && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_model_matcher_using_in_op.conf b/examples/rbac_model_matcher_using_in_op.conf new file mode 100644 index 0000000..227d149 --- /dev/null +++ b/examples/rbac_model_matcher_using_in_op.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.obj in ('data2', 'data3') \ No newline at end of file diff --git a/examples/rbac_model_matcher_using_in_op_bracket.conf b/examples/rbac_model_matcher_using_in_op_bracket.conf new file mode 100644 index 0000000..6ff819f --- /dev/null +++ b/examples/rbac_model_matcher_using_in_op_bracket.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.obj in ['data2', 'data3'] \ No newline at end of file diff --git a/examples/rbac_policy.csv b/examples/rbac_policy.csv new file mode 100644 index 0000000..f93d6df --- /dev/null +++ b/examples/rbac_policy.csv @@ -0,0 +1,5 @@ +p, alice, data1, read +p, bob, data2, write +p, data2_admin, data2, read +p, data2_admin, data2, write +g, alice, data2_admin \ No newline at end of file diff --git a/examples/rbac_with_abac_rule_model.conf b/examples/rbac_with_abac_rule_model.conf new file mode 100644 index 0000000..cd64692 --- /dev/null +++ b/examples/rbac_with_abac_rule_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act, ctx + +[policy_definition] +p = sub, obj, act, cond, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[matchers] +m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act) && eval(p.cond) diff --git a/examples/rbac_with_abac_rule_policy.csv b/examples/rbac_with_abac_rule_policy.csv new file mode 100644 index 0000000..2b1acf6 --- /dev/null +++ b/examples/rbac_with_abac_rule_policy.csv @@ -0,0 +1,20 @@ +# RBAC roles combined with a per-policy ABAC condition. +# +# p, role, object, action, cond, effect +# +# "cond" is a matcher expression evaluated against the ctx argument of the +# request. Write "true" for a policy that carries no condition. A cond may only +# read attributes that every request reaching it actually supplies: reading an +# attribute the ctx does not have makes Enforce return an error rather than a +# plain "denied", so pass a struct (or a map with every attribute filled in). + +p, employee, data1, read, r.ctx.Hour >= 9 && r.ctx.Hour < 18, allow +p, employee, data2, write, r.ctx.Secure == true, allow +p, admin, data3, read, true, allow + +# Deny overrides every allow above: a high-risk session gets nothing. +p, employee, *, *, r.ctx.Risk == 'high', deny + +g, admin, employee +g, alice, admin +g, bob, employee diff --git a/examples/rbac_with_all_pattern_model.conf b/examples/rbac_with_all_pattern_model.conf new file mode 100644 index 0000000..045bfa5 --- /dev/null +++ b/examples/rbac_with_all_pattern_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && g(r.obj, p.obj, r.dom) && r.dom == p.dom && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_all_pattern_policy.csv b/examples/rbac_with_all_pattern_policy.csv new file mode 100644 index 0000000..8097be8 --- /dev/null +++ b/examples/rbac_with_all_pattern_policy.csv @@ -0,0 +1,4 @@ +p, alice, domain1, book_group, read +p, alice, domain2, book_group, write + +g, /book/:id, book_group, * \ No newline at end of file diff --git a/examples/rbac_with_constraints_model.conf b/examples/rbac_with_constraints_model.conf new file mode 100644 index 0000000..2cf26ef --- /dev/null +++ b/examples/rbac_with_constraints_model.conf @@ -0,0 +1,20 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[constraint_definition] +c = sod("finance_requester", "finance_approver") +c2 = sodMax(["payroll_view", "payroll_edit", "payroll_approve"], 1) +c3 = roleMax("superadmin", 2) +c4 = rolePre("db_admin", "security_trained") + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act diff --git a/examples/rbac_with_cycle_policy.csv b/examples/rbac_with_cycle_policy.csv new file mode 100644 index 0000000..7c62032 --- /dev/null +++ b/examples/rbac_with_cycle_policy.csv @@ -0,0 +1,7 @@ +p, alice, data1, read +p, bob, data2, write +p, data2_admin, data2, read +p, data2_admin, data2, write +g, alice, data2_admin +g, data2_admin, super_admin +g, super_admin, alice diff --git a/examples/rbac_with_deny_model.conf b/examples/rbac_with_deny_model.conf new file mode 100644 index 0000000..33749f0 --- /dev/null +++ b/examples/rbac_with_deny_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_deny_policy.csv b/examples/rbac_with_deny_policy.csv new file mode 100644 index 0000000..0603db8 --- /dev/null +++ b/examples/rbac_with_deny_policy.csv @@ -0,0 +1,7 @@ +p, alice, data1, read, allow +p, bob, data2, write, allow +p, data2_admin, data2, read, allow +p, data2_admin, data2, write, allow +p, alice, data2, write, deny + +g, alice, data2_admin \ No newline at end of file diff --git a/examples/rbac_with_different_types_of_roles_model.conf b/examples/rbac_with_different_types_of_roles_model.conf new file mode 100644 index 0000000..069f234 --- /dev/null +++ b/examples/rbac_with_different_types_of_roles_model.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _, (_, _) +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, p.dom) && g2(r.obj, p.dom) && regexMatch(r.act, p.act) diff --git a/examples/rbac_with_different_types_of_roles_policy.csv b/examples/rbac_with_different_types_of_roles_policy.csv new file mode 100644 index 0000000..67d090b --- /dev/null +++ b/examples/rbac_with_different_types_of_roles_policy.csv @@ -0,0 +1,12 @@ +p, role:owner, domain1, _, (read|write) +p, role:developer, domain1, _, read + +p, role:owner, domain2, _, (read|write) +p, role:developer, domain2, _, read + +g, alice, role:owner, domain1, _, _ +g, bob, role:developer, domain2, _, 9999-12-30 00:00:00 +g, carol, role:owner, domain2, _, 0000-01-02 00:00:00 + +g2, data1, domain1 +g2, data2, domain2 diff --git a/examples/rbac_with_domain_pattern_model.conf b/examples/rbac_with_domain_pattern_model.conf new file mode 100644 index 0000000..774e441 --- /dev/null +++ b/examples/rbac_with_domain_pattern_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_domain_pattern_policy.csv b/examples/rbac_with_domain_pattern_policy.csv new file mode 100644 index 0000000..783f721 --- /dev/null +++ b/examples/rbac_with_domain_pattern_policy.csv @@ -0,0 +1,8 @@ +p, admin, domain1, data1, read +p, admin, domain1, data1, write +p, admin, domain2, data2, read +p, admin, domain2, data2, write +p, admin, *, data3, read + +g, alice, admin, * +g, bob, admin, domain2 \ No newline at end of file diff --git a/examples/rbac_with_domain_temporal_roles_model.conf b/examples/rbac_with_domain_temporal_roles_model.conf new file mode 100644 index 0000000..e1ab8a4 --- /dev/null +++ b/examples/rbac_with_domain_temporal_roles_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _, (_, _) + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_domain_temporal_roles_policy.csv b/examples/rbac_with_domain_temporal_roles_policy.csv new file mode 100644 index 0000000..8234b64 --- /dev/null +++ b/examples/rbac_with_domain_temporal_roles_policy.csv @@ -0,0 +1,24 @@ +p, alice, domain1, data1, read +p, alice, domain1, data1, write +p, data2_admin, domain2, data2, read +p, data2_admin, domain2, data2, write +p, data3_admin, domain3, data3, read +p, data3_admin, domain3, data3, write +p, data4_admin, domain4, data4, read +p, data4_admin, domain4, data4, write +p, data5_admin, domain5, data5, read +p, data5_admin, domain5, data5, write +p, data6_admin, domain6, data6, read +p, data6_admin, domain6, data6, write +p, data7_admin, domain7, data7, read +p, data7_admin, domain7, data7, write +p, data8_admin, domain8, data8, read +p, data8_admin, domain8, data8, write + +g, alice, data2_admin, domain2, 0000-01-01 00:00:00, 0000-01-02 00:00:00 +g, alice, data3_admin, domain3, 0000-01-01 00:00:00, 9999-12-30 00:00:00 +g, alice, data4_admin, domain4, _, _ +g, alice, data5_admin, domain5, _, 9999-12-30 00:00:00 +g, alice, data6_admin, domain6, _, 0000-01-02 00:00:00 +g, alice, data7_admin, domain7, 0000-01-01 00:00:00, _ +g, alice, data8_admin, domain8, 9999-12-30 00:00:00, _ \ No newline at end of file diff --git a/examples/rbac_with_domain_temporal_roles_transitive_policy.csv b/examples/rbac_with_domain_temporal_roles_transitive_policy.csv new file mode 100644 index 0000000..46df2c8 --- /dev/null +++ b/examples/rbac_with_domain_temporal_roles_transitive_policy.csv @@ -0,0 +1,5 @@ +p, role_top, domainx, data_top, read +p, role_solo, domainx, data_solo, read +g, alice, role_mid, domainx, _, _ +g, role_mid, role_top, domainx, 0000-01-01 00:00:00, 0000-01-02 00:00:00 +g, alice, role_solo, domainx, 0000-01-01 00:00:00, 0000-01-02 00:00:00 diff --git a/examples/rbac_with_domains_conditional_model.conf b/examples/rbac_with_domains_conditional_model.conf new file mode 100644 index 0000000..7dae4ad --- /dev/null +++ b/examples/rbac_with_domains_conditional_model.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _, (_, _) + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && \ +(keyMatch(r.act, p.act) || keyMatch2(r.act, p.act) || keyMatch3(r.act, p.act) || keyMatch4(r.act, p.act) || keyMatch5(r.act, p.act) || globMatch(r.act, p.act)) diff --git a/examples/rbac_with_domains_conditional_policy.csv b/examples/rbac_with_domains_conditional_policy.csv new file mode 100644 index 0000000..9f30081 --- /dev/null +++ b/examples/rbac_with_domains_conditional_policy.csv @@ -0,0 +1,13 @@ +p, test1, domain1, service1, /list +p, test1, domain1, service1, /get/:id/* +p, test1, domain1, service1, /add +p, test1, domain1, service1, /user/* +p, admin, domain1, service1, /* +p, qa1, domain2, service2, /broadcast +p, qa1, domain2, service2, /trip +p, qa1, domain2, service2, /notify +p, qa1, domain2, service2, /dynamic-sql + +g, alice, test1, domain1, _, 2999-12-30 00:00:00 +g, bob, qa1, domain2, _, 2999-12-30 00:00:00 +g, jack, test1, domain1, _, 0000-12-30 00:00:00 \ No newline at end of file diff --git a/examples/rbac_with_domains_model.conf b/examples/rbac_with_domains_model.conf new file mode 100644 index 0000000..57c3721 --- /dev/null +++ b/examples/rbac_with_domains_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_domains_policy.csv b/examples/rbac_with_domains_policy.csv new file mode 100644 index 0000000..8558d17 --- /dev/null +++ b/examples/rbac_with_domains_policy.csv @@ -0,0 +1,6 @@ +p, admin, domain1, data1, read +p, admin, domain1, data1, write +p, admin, domain2, data2, read +p, admin, domain2, data2, write +g, alice, admin, domain1 +g, bob, admin, domain2 \ No newline at end of file diff --git a/examples/rbac_with_domains_policy2.csv b/examples/rbac_with_domains_policy2.csv new file mode 100644 index 0000000..baa06f0 --- /dev/null +++ b/examples/rbac_with_domains_policy2.csv @@ -0,0 +1,9 @@ +p, admin, domain1, data1, read +p, admin, domain1, data1, write +p, admin, domain2, data2, read +p, admin, domain2, data2, write +p, user, domain3, data2, read +g, alice, admin, domain1 +g, alice, admin, domain2 +g, bob, admin, domain2 +g, bob, user, domain3 diff --git a/examples/rbac_with_hierarchy_policy.csv b/examples/rbac_with_hierarchy_policy.csv new file mode 100644 index 0000000..f722998 --- /dev/null +++ b/examples/rbac_with_hierarchy_policy.csv @@ -0,0 +1,10 @@ +p, alice, data1, read +p, bob, data2, write +p, data1_admin, data1, read +p, data1_admin, data1, write +p, data2_admin, data2, read +p, data2_admin, data2, write + +g, alice, admin +g, admin, data1_admin +g, admin, data2_admin \ No newline at end of file diff --git a/examples/rbac_with_hierarchy_with_domains_policy.csv b/examples/rbac_with_hierarchy_with_domains_policy.csv new file mode 100644 index 0000000..45d9173 --- /dev/null +++ b/examples/rbac_with_hierarchy_with_domains_policy.csv @@ -0,0 +1,11 @@ +p, role:reader, domain1, data1, read +p, role:writer, domain1, data1, write + +p, alice, domain1, data2, read +p, alice, domain2, data2, read + +g, role:global_admin, role:reader, domain1 +g, role:global_admin, role:writer, domain1 + +g, alice, role:global_admin, domain1 + diff --git a/examples/rbac_with_multiple_policy_model.conf b/examples/rbac_with_multiple_policy_model.conf new file mode 100644 index 0000000..d4581ac --- /dev/null +++ b/examples/rbac_with_multiple_policy_model.conf @@ -0,0 +1,17 @@ +[request_definition] +r = user, thing, action + +[policy_definition] +p = role, thing, action +p2 = role, action + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.user, p.role) && r.thing == p.thing && r.action == p.action +m2 = g(r.user, p2.role) && r.action == p.action + +[role_definition] +g = _,_ +g2 = _,_ \ No newline at end of file diff --git a/examples/rbac_with_multiple_policy_policy.csv b/examples/rbac_with_multiple_policy_policy.csv new file mode 100644 index 0000000..0afb588 --- /dev/null +++ b/examples/rbac_with_multiple_policy_policy.csv @@ -0,0 +1,9 @@ +p, user, /data, GET +p, admin, /data, POST + +p2, user, view +p2, admin, create + +g, admin, user +g, alice, admin +g2, alice, user \ No newline at end of file diff --git a/examples/rbac_with_not_deny_model.conf b/examples/rbac_with_not_deny_model.conf new file mode 100644 index 0000000..771f33d --- /dev/null +++ b/examples/rbac_with_not_deny_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = !some(where (p.eft == deny)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_pattern_model.conf b/examples/rbac_with_pattern_model.conf new file mode 100644 index 0000000..84580d9 --- /dev/null +++ b/examples/rbac_with_pattern_model.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && g2(r.obj, p.obj) && regexMatch(r.act, p.act) \ No newline at end of file diff --git a/examples/rbac_with_pattern_policy.csv b/examples/rbac_with_pattern_policy.csv new file mode 100644 index 0000000..bbe7687 --- /dev/null +++ b/examples/rbac_with_pattern_policy.csv @@ -0,0 +1,28 @@ +p, alice, /pen/1, GET +p, alice, /pen2/1, GET +p, book_admin, book_group, GET +p, pen_admin, pen_group, GET +p, *, pen3_group, GET + +p, /book/admin/:id, pen4_group, GET +g, /book/user/:id, /book/admin/1 + +p, /book/leader/2, pen4_group, POST +g, /book/user/:id, /book/leader/2 + +g, alice, book_admin +g, bob, pen_admin + +g, cathy, /book/1/2/3/4/5 +g, cathy, pen_admin + +g2, /book/*, book_group + +g2, /book/:id, book_group +g2, /pen/:id, pen_group + +g2, /book2/{id}, book_group +g2, /pen2/{id}, pen_group + +g2, /pen3/:id, pen3_group +g2, /pen4/:id, pen4_group \ No newline at end of file diff --git a/examples/rbac_with_resource_roles_model.conf b/examples/rbac_with_resource_roles_model.conf new file mode 100644 index 0000000..845bc6c --- /dev/null +++ b/examples/rbac_with_resource_roles_model.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_resource_roles_policy.csv b/examples/rbac_with_resource_roles_policy.csv new file mode 100644 index 0000000..b1d36da --- /dev/null +++ b/examples/rbac_with_resource_roles_policy.csv @@ -0,0 +1,7 @@ +p, alice, data1, read +p, bob, data2, write +p, data_group_admin, data_group, write + +g, alice, data_group_admin +g2, data1, data_group +g2, data2, data_group \ No newline at end of file diff --git a/examples/rbac_with_temporal_roles_model.conf b/examples/rbac_with_temporal_roles_model.conf new file mode 100644 index 0000000..feeae16 --- /dev/null +++ b/examples/rbac_with_temporal_roles_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _, (_, _) + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/rbac_with_temporal_roles_policy.csv b/examples/rbac_with_temporal_roles_policy.csv new file mode 100644 index 0000000..c8f77e7 --- /dev/null +++ b/examples/rbac_with_temporal_roles_policy.csv @@ -0,0 +1,24 @@ +p, alice, data1, read +p, alice, data1, write +p, data2_admin, data2, read +p, data2_admin, data2, write +p, data3_admin, data3, read +p, data3_admin, data3, write +p, data4_admin, data4, read +p, data4_admin, data4, write +p, data5_admin, data5, read +p, data5_admin, data5, write +p, data6_admin, data6, read +p, data6_admin, data6, write +p, data7_admin, data7, read +p, data7_admin, data7, write +p, data8_admin, data8, read +p, data8_admin, data8, write + +g, alice, data2_admin, 0000-01-01 00:00:00, 0000-01-02 00:00:00 +g, alice, data3_admin, 0000-01-01 00:00:00, 9999-12-30 00:00:00 +g, alice, data4_admin, _, _ +g, alice, data5_admin, _, 9999-12-30 00:00:00 +g, alice, data6_admin, _, 0000-01-02 00:00:00 +g, alice, data7_admin, 0000-01-01 00:00:00, _ +g, alice, data8_admin, 9999-12-30 00:00:00, _ \ No newline at end of file diff --git a/examples/rebac_model.conf b/examples/rebac_model.conf new file mode 100644 index 0000000..7630950 --- /dev/null +++ b/examples/rebac_model.conf @@ -0,0 +1,15 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = role, obj_type, act + +[role_definition] +g = _, _, _ +g2 = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, r.obj, p.role) && g2(r.obj, p.obj_type) && r.act == p.act \ No newline at end of file diff --git a/examples/rebac_policy.csv b/examples/rebac_policy.csv new file mode 100644 index 0000000..998652b --- /dev/null +++ b/examples/rebac_policy.csv @@ -0,0 +1,7 @@ +p, collaborator, doc, read + +g, alice, doc1, collaborator +g, bob, doc2, collaborator + +g2, doc1, doc +g2, doc2, doc \ No newline at end of file diff --git a/examples/subject_priority_model.conf b/examples/subject_priority_model.conf new file mode 100644 index 0000000..77b8c4e --- /dev/null +++ b/examples/subject_priority_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = subjectPriority(p.eft) || deny + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/subject_priority_model_with_domain.conf b/examples/subject_priority_model_with_domain.conf new file mode 100644 index 0000000..84ec518 --- /dev/null +++ b/examples/subject_priority_model_with_domain.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, dom, act + +[policy_definition] +p = sub, obj, dom, act, eft #sub can't change position,must be first + +[role_definition] +g = _, _, _ + +[policy_effect] +e = subjectPriority(p.eft) || deny + +[matchers] +m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act \ No newline at end of file diff --git a/examples/subject_priority_policy.csv b/examples/subject_priority_policy.csv new file mode 100644 index 0000000..24a4422 --- /dev/null +++ b/examples/subject_priority_policy.csv @@ -0,0 +1,16 @@ +p, root, data1, read, deny +p, admin, data1, read, deny + +p, editor, data1, read, deny +p, subscriber, data1, read, deny + +p, jane, data1, read, allow +p, alice, data1, read, allow + +g, admin, root + +g, editor, admin +g, subscriber, admin + +g, jane, editor +g, alice, subscriber \ No newline at end of file diff --git a/examples/subject_priority_policy_with_domain.csv b/examples/subject_priority_policy_with_domain.csv new file mode 100644 index 0000000..c4859ec --- /dev/null +++ b/examples/subject_priority_policy_with_domain.csv @@ -0,0 +1,7 @@ +p, admin, data1, domain1, write, deny +p, alice, data1, domain1, write, allow +p, admin, data2, domain2, write, deny +p, bob, data2, domain2, write, allow + +g, alice, admin, domain1 +g, bob, admin, domain2 \ No newline at end of file diff --git a/examples/syntax_matcher_model.conf b/examples/syntax_matcher_model.conf new file mode 100644 index 0000000..f99a385 --- /dev/null +++ b/examples/syntax_matcher_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p_eft == allow)) + +[matchers] +m = r.sub == "a.p.p.l.e" diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..c60ccd0 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,212 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" + + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" + "github.com/casbin/casbin/v3/util" +) + +func TestInitFilteredAdapter(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv") + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + + // policy should not be loaded yet + testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, false) +} + +func TestLoadFilteredPolicy(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv") + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + if err := e.LoadPolicy(); err != nil { + t.Errorf("unexpected error in LoadPolicy: %v", err) + } + + // validate initial conditions + testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, true) + testHasPolicy(t, e, []string{"admin", "domain2", "data2", "read"}, true) + + if err := e.LoadFilteredPolicy(&fileadapter.Filter{ + P: []string{"", "domain1"}, + G: []string{"", "", "domain1"}, + }); err != nil { + t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) + } + if !e.IsFiltered() { + t.Errorf("adapter did not set the filtered flag correctly") + } + + // only policies for domain1 should be loaded + testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, true) + testHasPolicy(t, e, []string{"admin", "domain2", "data2", "read"}, false) + + if err := e.SavePolicy(); err == nil { + t.Errorf("enforcer did not prevent saving filtered policy") + } + if err := e.GetAdapter().SavePolicy(e.GetModel()); err == nil { + t.Errorf("adapter did not prevent saving filtered policy") + } +} + +func TestLoadMoreTypeFilteredPolicy(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_pattern_policy.csv") + _ = e.InitWithAdapter("examples/rbac_with_pattern_model.conf", adapter) + if err := e.LoadPolicy(); err != nil { + t.Errorf("unexpected error in LoadPolicy: %v", err) + } + e.AddNamedMatchingFunc("g2", "matching func", util.KeyMatch2) + _ = e.BuildRoleLinks() + + testEnforce(t, e, "alice", "/book/1", "GET", true) + + // validate initial conditions + testHasPolicy(t, e, []string{"book_admin", "book_group", "GET"}, true) + testHasPolicy(t, e, []string{"pen_admin", "pen_group", "GET"}, true) + + if err := e.LoadFilteredPolicy(&fileadapter.Filter{ + P: []string{"book_admin"}, + G: []string{"alice"}, + G2: []string{"", "book_group"}, + }); err != nil { + t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) + } + if !e.IsFiltered() { + t.Errorf("adapter did not set the filtered flag correctly") + } + + testHasPolicy(t, e, []string{"alice", "/pen/1", "GET"}, false) + testHasPolicy(t, e, []string{"alice", "/pen2/1", "GET"}, false) + testHasPolicy(t, e, []string{"pen_admin", "pen_group", "GET"}, false) + testHasGroupingPolicy(t, e, []string{"alice", "book_admin"}, true) + testHasGroupingPolicy(t, e, []string{"bob", "pen_admin"}, false) + testHasGroupingPolicy(t, e, []string{"cathy", "pen_admin"}, false) + testHasGroupingPolicy(t, e, []string{"cathy", "/book/1/2/3/4/5"}, false) + + testEnforce(t, e, "alice", "/book/1", "GET", true) + testEnforce(t, e, "alice", "/pen/1", "GET", false) +} + +func TestAppendFilteredPolicy(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv") + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + if err := e.LoadPolicy(); err != nil { + t.Errorf("unexpected error in LoadPolicy: %v", err) + } + + // validate initial conditions + testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, true) + testHasPolicy(t, e, []string{"admin", "domain2", "data2", "read"}, true) + + if err := e.LoadFilteredPolicy(&fileadapter.Filter{ + P: []string{"", "domain1"}, + G: []string{"", "", "domain1"}, + }); err != nil { + t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) + } + if !e.IsFiltered() { + t.Errorf("adapter did not set the filtered flag correctly") + } + + // only policies for domain1 should be loaded + testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, true) + testHasPolicy(t, e, []string{"admin", "domain2", "data2", "read"}, false) + + // disable clear policy and load second domain + if err := e.LoadIncrementalFilteredPolicy(&fileadapter.Filter{ + P: []string{"", "domain2"}, + G: []string{"", "", "domain2"}, + }); err != nil { + t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) + } + + // both domain policies should be loaded + testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, true) + testHasPolicy(t, e, []string{"admin", "domain2", "data2", "read"}, true) +} + +func TestFilteredPolicyInvalidFilter(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv") + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + + if err := e.LoadFilteredPolicy([]string{"", "domain1"}); err == nil { + t.Errorf("expected error in LoadFilteredPolicy, but got nil") + } +} + +func TestFilteredPolicyEmptyFilter(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv") + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + + if err := e.LoadFilteredPolicy(nil); err != nil { + t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) + } + if e.IsFiltered() { + t.Errorf("adapter did not reset the filtered flag correctly") + } + if err := e.SavePolicy(); err != nil { + t.Errorf("unexpected error in SavePolicy: %v", err) + } +} + +func TestUnsupportedFilteredPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + err := e.LoadFilteredPolicy(&fileadapter.Filter{ + P: []string{"", "domain1"}, + G: []string{"", "", "domain1"}, + }) + if err == nil { + t.Errorf("encorcer should have reported incompatibility error") + } +} + +func TestFilteredAdapterEmptyFilepath(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("") + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + + if err := e.LoadFilteredPolicy(nil); err != nil { + t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) + } +} + +func TestFilteredAdapterInvalidFilepath(t *testing.T) { + e, _ := NewEnforcer() + + adapter := fileadapter.NewFilteredAdapter("examples/does_not_exist_policy.csv") + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + + if err := e.LoadFilteredPolicy(nil); err == nil { + t.Errorf("expected error in LoadFilteredPolicy, but got nil") + } +} diff --git a/frontend.go b/frontend.go new file mode 100644 index 0000000..691a7a0 --- /dev/null +++ b/frontend.go @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "bytes" + "encoding/json" +) + +func CasbinJsGetPermissionForUser(e IEnforcer, user string) (string, error) { + model := e.GetModel() + m := map[string]interface{}{} + + m["m"] = model.ToText() + + pRules := [][]string{} + for ptype := range model["p"] { + policies, err := model.GetPolicy("p", ptype) + if err != nil { + return "", err + } + for _, rules := range policies { + pRules = append(pRules, append([]string{ptype}, rules...)) + } + } + m["p"] = pRules + + gRules := [][]string{} + for ptype := range model["g"] { + policies, err := model.GetPolicy("g", ptype) + if err != nil { + return "", err + } + for _, rules := range policies { + gRules = append(gRules, append([]string{ptype}, rules...)) + } + } + m["g"] = gRules + + result := bytes.NewBuffer([]byte{}) + encoder := json.NewEncoder(result) + encoder.SetEscapeHTML(false) + err := encoder.Encode(m) + return result.String(), err +} diff --git a/frontend_old.go b/frontend_old.go new file mode 100644 index 0000000..6a39394 --- /dev/null +++ b/frontend_old.go @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import "encoding/json" + +func CasbinJsGetPermissionForUserOld(e IEnforcer, user string) ([]byte, error) { + policy, err := e.GetImplicitPermissionsForUser(user) + if err != nil { + return nil, err + } + permission := make(map[string][]string) + for i := 0; i < len(policy); i++ { + permission[policy[i][2]] = append(permission[policy[i][2]], policy[i][1]) + } + b, _ := json.Marshal(permission) + return b, nil +} diff --git a/frontend_old_test.go b/frontend_old_test.go new file mode 100644 index 0000000..47b0296 --- /dev/null +++ b/frontend_old_test.go @@ -0,0 +1,96 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "encoding/json" + "testing" +) + +func contains(arr []string, target string) bool { + for _, item := range arr { + if item == target { + return true + } + } + return false +} + +func TestCasbinJsGetPermissionForUserOld(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + panic(err) + } + targetStr, _ := CasbinJsGetPermissionForUserOld(e, "alice") + t.Log("GetPermissionForUser Alice", string(targetStr)) + aliceTarget := make(map[string][]string) + err = json.Unmarshal(targetStr, &aliceTarget) + if err != nil { + t.Errorf("Test error: %s", err) + } + perm, ok := aliceTarget["read"] + if !ok { + t.Errorf("Test error: Alice doesn't have read permission") + } + if !contains(perm, "data1") { + t.Errorf("Test error: Alice cannot read data1") + } + if !contains(perm, "data2") { + t.Errorf("Test error: Alice cannot read data2") + } + perm, ok = aliceTarget["write"] + if !ok { + t.Errorf("Test error: Alice doesn't have write permission") + } + if contains(perm, "data1") { + t.Errorf("Test error: Alice can write data1") + } + if !contains(perm, "data2") { + t.Errorf("Test error: Alice cannot write data2") + } + + targetStr, _ = CasbinJsGetPermissionForUserOld(e, "bob") + t.Log("GetPermissionForUser Bob", string(targetStr)) + bobTarget := make(map[string][]string) + err = json.Unmarshal(targetStr, &bobTarget) + if err != nil { + t.Errorf("Test error: %s", err) + } + _, ok = bobTarget["read"] + if ok { + t.Errorf("Test error: Bob has read permission") + } + perm, ok = bobTarget["write"] + if !ok { + t.Errorf("Test error: Bob doesn't have permission") + } + if !contains(perm, "data2") { + t.Errorf("Test error: Bob cannot write data2") + } + if contains(perm, "data1") { + t.Errorf("Test error: Bob can write data1") + } + if contains(perm, "data_not_exist") { + t.Errorf("Test error: Bob can access a non-existing data") + } + + _, ok = bobTarget["rm_rf"] + if ok { + t.Errorf("Someone can have a non-existing action (rm -rf)") + } +} diff --git a/frontend_test.go b/frontend_test.go new file mode 100644 index 0000000..bdcec01 --- /dev/null +++ b/frontend_test.go @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "encoding/json" + "os" + "regexp" + "strings" + "testing" +) + +func TestCasbinJsGetPermissionForUser(t *testing.T) { + e, err := NewSyncedEnforcer("examples/rbac_model.conf", "examples/rbac_with_hierarchy_policy.csv") + if err != nil { + panic(err) + } + receivedString, err := CasbinJsGetPermissionForUser(e, "alice") // make sure CasbinJsGetPermissionForUser can be used with a SyncedEnforcer. + if err != nil { + t.Errorf("Test error: %s", err) + } + received := map[string]interface{}{} + err = json.Unmarshal([]byte(receivedString), &received) + if err != nil { + t.Errorf("Test error: %s", err) + } + expectedModel, err := os.ReadFile("examples/rbac_model.conf") + if err != nil { + t.Errorf("Test error: %s", err) + } + // Normalize line endings to \n for cross-platform compatibility + expectedModelStr := regexp.MustCompile("(\r?\n)+").ReplaceAllString(string(expectedModel), "\n") + actualModelStr := strings.TrimSpace(received["m"].(string)) + expectedModelStr = strings.TrimSpace(expectedModelStr) + + if actualModelStr != expectedModelStr { + t.Errorf("%s supposed to be %s", actualModelStr, expectedModelStr) + } + + expectedPolicies, err := os.ReadFile("examples/rbac_with_hierarchy_policy.csv") + if err != nil { + t.Errorf("Test error: %s", err) + } + expectedPoliciesItem := regexp.MustCompile(",|\n").Split(string(expectedPolicies), -1) + i := 0 + for _, sArr := range received["p"].([]interface{}) { + for _, s := range sArr.([]interface{}) { + if strings.TrimSpace(s.(string)) != strings.TrimSpace(expectedPoliciesItem[i]) { + t.Errorf("%s supposed to be %s", strings.TrimSpace(s.(string)), strings.TrimSpace(expectedPoliciesItem[i])) + } + i++ + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c46f727 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/casbin/casbin/v3 + +require ( + github.com/bmatcuk/doublestar/v4 v4.6.1 + github.com/casbin/govaluate v1.3.0 + github.com/google/uuid v1.6.0 +) + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f3a1c7 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/internal_api.go b/internal_api.go new file mode 100644 index 0000000..04883af --- /dev/null +++ b/internal_api.go @@ -0,0 +1,555 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + + Err "github.com/casbin/casbin/v3/errors" + "github.com/casbin/casbin/v3/log" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +const ( + notImplemented = "not implemented" +) + +func (e *Enforcer) shouldPersist() bool { + return e.adapter != nil && e.autoSave +} + +func (e *Enforcer) shouldNotify() bool { + return e.watcher != nil && e.autoNotifyWatcher +} + +// validateConstraintsForGroupingPolicy validates constraints for grouping policy changes. +// It returns an error if constraint validation fails. +func (e *Enforcer) validateConstraintsForGroupingPolicy() error { + return e.model.ValidateConstraints() +} + +// addPolicy adds a rule to the current policy. +func (e *Enforcer) addPolicyWithoutNotify(sec string, ptype string, rule []string) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.AddPolicies(sec, ptype, [][]string{rule}) + } + + hasPolicy, err := e.model.HasPolicy(sec, ptype, rule) + if hasPolicy || err != nil { + return false, err + } + + if e.shouldPersist() { + if err = e.adapter.AddPolicy(sec, ptype, rule); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + err = e.model.AddPolicy(sec, ptype, rule) + if err != nil { + return false, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{rule}) + if err != nil { + return true, err + } + + // Validate constraints after adding grouping policy + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return true, nil +} + +// addPoliciesWithoutNotify adds rules to the current policy without notify +// If autoRemoveRepeat == true, existing rules are automatically filtered +// Otherwise, false is returned directly. +func (e *Enforcer) addPoliciesWithoutNotify(sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.AddPolicies(sec, ptype, rules) + } + + if !autoRemoveRepeat { + hasPolicies, err := e.model.HasPolicies(sec, ptype, rules) + if hasPolicies || err != nil { + return false, err + } + } + + if e.shouldPersist() { + if err := e.adapter.(persist.BatchAdapter).AddPolicies(sec, ptype, rules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + err := e.model.AddPolicies(sec, ptype, rules) + if err != nil { + return false, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, rules) + if err != nil { + return true, err + } + + err = e.BuildIncrementalConditionalRoleLinks(model.PolicyAdd, ptype, rules) + if err != nil { + return true, err + } + + // Validate constraints after adding grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return true, nil +} + +// removePolicy removes a rule from the current policy. +func (e *Enforcer) removePolicyWithoutNotify(sec string, ptype string, rule []string) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.RemovePolicies(sec, ptype, [][]string{rule}) + } + + if e.shouldPersist() { + if err := e.adapter.RemovePolicy(sec, ptype, rule); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + ruleRemoved, err := e.model.RemovePolicy(sec, ptype, rule) + if !ruleRemoved || err != nil { + return ruleRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{rule}) + if err != nil { + return ruleRemoved, err + } + + // Validate constraints after removing grouping policy + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return ruleRemoved, nil +} + +func (e *Enforcer) updatePolicyWithoutNotify(sec string, ptype string, oldRule []string, newRule []string) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.UpdatePolicy(sec, ptype, oldRule, newRule) + } + + if e.shouldPersist() { + if err := e.adapter.(persist.UpdatableAdapter).UpdatePolicy(sec, ptype, oldRule, newRule); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + ruleUpdated, err := e.model.UpdatePolicy(sec, ptype, oldRule, newRule) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{oldRule}) // remove the old rule + if err != nil { + return ruleUpdated, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{newRule}) // add the new rule + if err != nil { + return ruleUpdated, err + } + + // Validate constraints after updating grouping policy + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return ruleUpdated, nil +} + +func (e *Enforcer) updatePoliciesWithoutNotify(sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + if len(newRules) != len(oldRules) { + return false, fmt.Errorf("the length of oldRules should be equal to the length of newRules, but got the length of oldRules is %d, the length of newRules is %d", len(oldRules), len(newRules)) + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.UpdatePolicies(sec, ptype, oldRules, newRules) + } + + if e.shouldPersist() { + if err := e.adapter.(persist.UpdatableAdapter).UpdatePolicies(sec, ptype, oldRules, newRules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + ruleUpdated, err := e.model.UpdatePolicies(sec, ptype, oldRules, newRules) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules + if err != nil { + return ruleUpdated, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules + if err != nil { + return ruleUpdated, err + } + + // Validate constraints after updating grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return ruleUpdated, nil +} + +// removePolicies removes rules from the current policy. +func (e *Enforcer) removePoliciesWithoutNotify(sec string, ptype string, rules [][]string) (bool, error) { + if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); !hasPolicies || err != nil { + return hasPolicies, err + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.RemovePolicies(sec, ptype, rules) + } + + if e.shouldPersist() { + if err := e.adapter.(persist.BatchAdapter).RemovePolicies(sec, ptype, rules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + rulesRemoved, err := e.model.RemovePolicies(sec, ptype, rules) + if !rulesRemoved || err != nil { + return rulesRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, rules) + if err != nil { + return rulesRemoved, err + } + + // Validate constraints after removing grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + return rulesRemoved, nil +} + +// removeFilteredPolicy removes rules based on field filters from the current policy. +func (e *Enforcer) removeFilteredPolicyWithoutNotify(sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) { + if len(fieldValues) == 0 { + return false, Err.ErrInvalidFieldValuesParameter + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + } + + if e.shouldPersist() { + if err := e.adapter.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + ruleRemoved, effects, err := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + if !ruleRemoved || err != nil { + return ruleRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, effects) + if err != nil { + return ruleRemoved, err + } + + // Validate constraints after removing filtered grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return ruleRemoved, nil +} + +func (e *Enforcer) updateFilteredPoliciesWithoutNotify(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { + var ( + oldRules [][]string + err error + ) + + if _, err = e.model.GetAssertion(sec, ptype); err != nil { + return oldRules, err + } + + if e.shouldPersist() { + if oldRules, err = e.adapter.(persist.UpdatableAdapter).UpdateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues...); err != nil { + if err.Error() != notImplemented { + return nil, err + } + } + // For compatibility, because some adapters return oldRules containing ptype, see https://github.com/casbin/xorm-adapter/issues/49 + for i, oldRule := range oldRules { + if len(oldRules[i]) == len(e.model[sec][ptype].Tokens)+1 { + oldRules[i] = oldRule[1:] + } + } + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return oldRules, e.dispatcher.UpdateFilteredPolicies(sec, ptype, oldRules, newRules) + } + + ruleChanged, err := e.model.RemovePolicies(sec, ptype, oldRules) + if err != nil { + return oldRules, err + } + err = e.model.AddPolicies(sec, ptype, newRules) + if err != nil { + return oldRules, err + } + ruleChanged = ruleChanged && len(newRules) != 0 + if !ruleChanged { + return make([][]string, 0), nil + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules + if err != nil { + return oldRules, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules + if err != nil { + return oldRules, err + } + + // Validate constraints after updating filtered grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return oldRules, err + } + } + + return oldRules, nil +} + +// addPolicy adds a rule to the current policy. +func (e *Enforcer) addPolicy(sec string, ptype string, rule []string) (bool, error) { + ok, err := e.logPolicyOperation(log.EventAddPolicy, sec, rule, func() (bool, error) { + return e.addPolicyWithoutNotify(sec, ptype, rule) + }) + + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var notifyErr error + if watcher, isWatcherEx := e.watcher.(persist.WatcherEx); isWatcherEx { + notifyErr = watcher.UpdateForAddPolicy(sec, ptype, rule...) + } else { + notifyErr = e.watcher.Update() + } + return true, notifyErr + } + + return true, nil +} + +// addPolicies adds rules to the current policy. +// If autoRemoveRepeat == true, existing rules are automatically filtered +// Otherwise, false is returned directly. +func (e *Enforcer) addPolicies(sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) { + ok, err := e.addPoliciesWithoutNotify(sec, ptype, rules, autoRemoveRepeat) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForAddPolicies(sec, ptype, rules...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +// removePolicy removes a rule from the current policy. +func (e *Enforcer) removePolicy(sec string, ptype string, rule []string) (bool, error) { + ok, err := e.logPolicyOperation(log.EventRemovePolicy, sec, rule, func() (bool, error) { + return e.removePolicyWithoutNotify(sec, ptype, rule) + }) + + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var notifyErr error + if watcher, isWatcherEx := e.watcher.(persist.WatcherEx); isWatcherEx { + notifyErr = watcher.UpdateForRemovePolicy(sec, ptype, rule...) + } else { + notifyErr = e.watcher.Update() + } + return true, notifyErr + } + + return true, nil +} + +func (e *Enforcer) updatePolicy(sec string, ptype string, oldRule []string, newRule []string) (bool, error) { + ok, err := e.updatePolicyWithoutNotify(sec, ptype, oldRule, newRule) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicy(sec, ptype, oldRule, newRule) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *Enforcer) updatePolicies(sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + ok, err := e.updatePoliciesWithoutNotify(sec, ptype, oldRules, newRules) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +// removePolicies removes rules from the current policy. +func (e *Enforcer) removePolicies(sec string, ptype string, rules [][]string) (bool, error) { + ok, err := e.removePoliciesWithoutNotify(sec, ptype, rules) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForRemovePolicies(sec, ptype, rules...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +// removeFilteredPolicy removes rules based on field filters from the current policy. +func (e *Enforcer) removeFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) { + ok, err := e.removeFilteredPolicyWithoutNotify(sec, ptype, fieldIndex, fieldValues) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *Enforcer) updateFilteredPolicies(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + oldRules, err := e.updateFilteredPoliciesWithoutNotify(sec, ptype, newRules, fieldIndex, fieldValues...) + ok := len(oldRules) != 0 + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *Enforcer) GetFieldIndex(ptype string, field string) (int, error) { + return e.model.GetFieldIndex(ptype, field) +} + +func (e *Enforcer) SetFieldIndex(ptype string, field string, index int) { + assertion := e.model["p"][ptype] + assertion.FieldIndexMutex.Lock() + assertion.FieldIndexMap[field] = index + assertion.FieldIndexMutex.Unlock() +} diff --git a/lbac_test.go b/lbac_test.go new file mode 100644 index 0000000..e19c9d2 --- /dev/null +++ b/lbac_test.go @@ -0,0 +1,59 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +func testEnforceLBAC(t *testing.T, e *Enforcer, sub string, subConf, subInteg float64, obj string, objConf, objInteg float64, act string, res bool) { + t.Helper() + if myRes, err := e.Enforce(sub, subConf, subInteg, obj, objConf, objInteg, act); err != nil { + t.Errorf("Enforce Error: %s", err) + } else if myRes != res { + t.Errorf("%s, conf=%v, integ=%v, %s, conf=%v, integ=%v, %s: %t, supposed to be %t", sub, subConf, subInteg, obj, objConf, objInteg, act, myRes, res) + } +} + +func TestLBACModel(t *testing.T) { + e, _ := NewEnforcer("examples/lbac_model.conf") + + t.Log("Testing normal read operation scenarios") + testEnforceLBAC(t, e, "admin", 5, 5, "file_topsecret", 3, 3, "read", true) // both high + testEnforceLBAC(t, e, "manager", 4, 4, "file_secret", 4, 2, "read", true) // confidentiality equal, integrity higher + testEnforceLBAC(t, e, "staff", 3, 3, "file_internal", 2, 3, "read", true) // confidentiality higher, integrity equal + testEnforceLBAC(t, e, "guest", 2, 2, "file_public", 2, 2, "read", true) // both dimensions equal + + t.Log("Testing read operation violation scenarios") + testEnforceLBAC(t, e, "staff", 3, 3, "file_secret", 4, 2, "read", false) // insufficient confidentiality level + testEnforceLBAC(t, e, "manager", 4, 4, "file_sensitive", 3, 5, "read", false) // insufficient integrity level + testEnforceLBAC(t, e, "guest", 2, 2, "file_internal", 3, 1, "read", false) // insufficient confidentiality level + testEnforceLBAC(t, e, "staff", 3, 3, "file_protected", 1, 4, "read", false) // insufficient integrity level + + t.Log("Testing normal write operation scenarios") + testEnforceLBAC(t, e, "guest", 2, 2, "file_public", 2, 2, "write", true) // both dimensions equal + testEnforceLBAC(t, e, "staff", 3, 3, "file_internal", 5, 4, "write", true) // both low + testEnforceLBAC(t, e, "manager", 4, 4, "file_secret", 4, 5, "write", true) // confidentiality equal, integrity low + testEnforceLBAC(t, e, "admin", 5, 5, "file_archive", 5, 5, "write", true) // both dimensions equal + + t.Log("Testing write operation violation scenarios") + testEnforceLBAC(t, e, "manager", 4, 4, "file_internal", 3, 5, "write", false) // confidentiality level too high + testEnforceLBAC(t, e, "staff", 3, 3, "file_public", 2, 2, "write", false) // both dimensions too high + testEnforceLBAC(t, e, "admin", 5, 5, "file_secret", 5, 4, "write", false) // integrity level too high + testEnforceLBAC(t, e, "guest", 2, 2, "file_private", 1, 3, "write", false) // confidentiality level too high +} diff --git a/log/default_logger.go b/log/default_logger.go new file mode 100644 index 0000000..f488d57 --- /dev/null +++ b/log/default_logger.go @@ -0,0 +1,142 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package log + +import ( + "fmt" + "io" + "os" + "strings" + "time" +) + +// DefaultLogger is the default implementation of the Logger interface. +type DefaultLogger struct { + output io.Writer + eventTypes map[EventType]bool + logCallback func(entry *LogEntry) error +} + +// NewDefaultLogger creates a new DefaultLogger instance. +// If no output is set via SetOutput, it defaults to os.Stdout. +func NewDefaultLogger() *DefaultLogger { + return &DefaultLogger{ + output: os.Stdout, + eventTypes: make(map[EventType]bool), + } +} + +// SetOutput sets the output destination for the logger. +// It can be set to a buffer or any io.Writer. +func (l *DefaultLogger) SetOutput(w io.Writer) { + if w != nil { + l.output = w + } +} + +// SetEventTypes sets the event types that should be logged. +// Only events matching these types will have IsActive set to true. +func (l *DefaultLogger) SetEventTypes(eventTypes []EventType) error { + l.eventTypes = make(map[EventType]bool) + for _, et := range eventTypes { + l.eventTypes[et] = true + } + return nil +} + +// OnBeforeEvent is called before an event occurs. +// It sets the StartTime and determines if the event should be active based on configured event types. +func (l *DefaultLogger) OnBeforeEvent(entry *LogEntry) error { + if entry == nil { + return fmt.Errorf("log entry is nil") + } + + entry.StartTime = time.Now() + + // Set IsActive based on whether this event type is enabled + // If no event types are configured, all events are considered active + if len(l.eventTypes) == 0 { + entry.IsActive = true + } else { + entry.IsActive = l.eventTypes[entry.EventType] + } + + return nil +} + +// OnAfterEvent is called after an event completes. +// It calculates the duration, logs the entry if active, and calls the user callback if set. +func (l *DefaultLogger) OnAfterEvent(entry *LogEntry) error { + if entry == nil { + return fmt.Errorf("log entry is nil") + } + + entry.EndTime = time.Now() + entry.Duration = entry.EndTime.Sub(entry.StartTime) + + // Only log if the event is active + if entry.IsActive && l.output != nil { + if err := l.writeLog(entry); err != nil { + return err + } + } + + // Call user-provided callback if set + if l.logCallback != nil { + if err := l.logCallback(entry); err != nil { + return err + } + } + + return nil +} + +// SetLogCallback sets a user-provided callback function. +// The callback is called at the end of OnAfterEvent. +func (l *DefaultLogger) SetLogCallback(callback func(entry *LogEntry) error) error { + l.logCallback = callback + return nil +} + +// writeLog writes the log entry to the configured output. +func (l *DefaultLogger) writeLog(entry *LogEntry) error { + var logMessage string + + switch entry.EventType { + case EventEnforce: + logMessage = fmt.Sprintf("[%s] Enforce: subject=%s, object=%s, action=%s, domain=%s, allowed=%v, duration=%v\n", + entry.EventType, entry.Subject, entry.Object, entry.Action, entry.Domain, entry.Allowed, entry.Duration) + case EventAddPolicy, EventRemovePolicy: + logMessage = fmt.Sprintf("[%s] RuleCount=%d, duration=%v\n", + entry.EventType, entry.RuleCount, entry.Duration) + case EventLoadPolicy, EventSavePolicy: + logMessage = fmt.Sprintf("[%s] RuleCount=%d, duration=%v\n", + entry.EventType, entry.RuleCount, entry.Duration) + default: + logMessage = fmt.Sprintf("[%s] duration=%v\n", + entry.EventType, entry.Duration) + } + + if entry.Error != nil { + logMessage = strings.TrimSuffix(logMessage, "\n") + logMessage = fmt.Sprintf("%s Error: %v\n", logMessage, entry.Error) + } + + _, err := l.output.Write([]byte(logMessage)) + return err +} diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 0000000..5b8b995 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package log + +// Logger defines the interface for event-driven logging in Casbin. +type Logger interface { + SetEventTypes([]EventType) error + // OnBeforeEvent is called before an event occurs and returns a handle for context. + OnBeforeEvent(entry *LogEntry) error + // OnAfterEvent is called after an event completes with the handle and final entry. + OnAfterEvent(entry *LogEntry) error + + SetLogCallback(func(entry *LogEntry) error) error +} diff --git a/log/types.go b/log/types.go new file mode 100644 index 0000000..fd078a9 --- /dev/null +++ b/log/types.go @@ -0,0 +1,63 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package log + +import "time" + +// EventType represents the type of logging event. +type EventType string + +// Event type constants. +const ( + EventEnforce EventType = "enforce" + EventAddPolicy EventType = "addPolicy" + EventRemovePolicy EventType = "removePolicy" + EventLoadPolicy EventType = "loadPolicy" + EventSavePolicy EventType = "savePolicy" +) + +// LogEntry represents a complete log entry for a Casbin event. +type LogEntry struct { + IsActive bool + // EventType is the type of the event being logged. + EventType EventType + + StartTime time.Time + EndTime time.Time + Duration time.Duration + + // Enforce parameters. + // Subject is the user or entity requesting access. + Subject string + // Object is the resource being accessed. + Object string + // Action is the operation being performed. + Action string + // Domain is the domain/tenant for multi-tenant scenarios. + Domain string + // Allowed indicates whether the enforcement request was allowed. + Allowed bool + + // Rules contains the policy rules involved in the operation. + Rules [][]string + // RuleCount is the number of rules affected by the operation. + RuleCount int + + // Error contains any error that occurred during the event. + Error error +} diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 0000000..638a143 --- /dev/null +++ b/logger_test.go @@ -0,0 +1,251 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "bytes" + "strings" + "testing" + + "github.com/casbin/casbin/v3/log" +) + +func verifyBufferOutput(t *testing.T, logOutput string) { + t.Helper() + expectedEvents := []string{"[enforce]", "[addPolicy]", "[removePolicy]", "[savePolicy]", "[loadPolicy]"} + for _, event := range expectedEvents { + if !strings.Contains(logOutput, event) { + t.Errorf("Expected log output to contain %s event", event) + } + } +} + +func verifyCallbackEntries(t *testing.T, entries []*log.LogEntry) { + t.Helper() + found := map[log.EventType]bool{} + + for _, entry := range entries { + found[entry.EventType] = true + switch entry.EventType { + case log.EventEnforce: + if entry.Subject == "" && entry.Object == "" && entry.Action == "" { + t.Errorf("Expected enforce entry to have subject, object, and action") + } + case log.EventAddPolicy, log.EventRemovePolicy: + if entry.RuleCount != 1 { + t.Errorf("Expected %s entry to have RuleCount=1, got %d", entry.EventType, entry.RuleCount) + } + case log.EventSavePolicy, log.EventLoadPolicy: + if entry.RuleCount == 0 { + t.Errorf("Expected %s entry to have RuleCount>0", entry.EventType) + } + } + } + + requiredEvents := []log.EventType{ + log.EventEnforce, log.EventAddPolicy, log.EventRemovePolicy, + log.EventSavePolicy, log.EventLoadPolicy, + } + for _, eventType := range requiredEvents { + if !found[eventType] { + t.Errorf("Expected to find %s in callback entries", eventType) + } + } +} + +func TestEnforcerWithDefaultLogger(t *testing.T) { + // Create enforcer with RBAC model and policy + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Create a buffer to capture log output + var buf bytes.Buffer + logger := log.NewDefaultLogger() + logger.SetOutput(&buf) + + // Set up a callback to track log entries + var callbackEntries []*log.LogEntry + err = logger.SetLogCallback(func(entry *log.LogEntry) error { + // Create a copy of the entry to store + entryCopy := *entry + callbackEntries = append(callbackEntries, &entryCopy) + return nil + }) + if err != nil { + t.Fatalf("Failed to set log callback: %v", err) + } + + // Set the logger on the enforcer + e.SetLogger(logger) + + // Test Enforce events + if result, err := e.Enforce("alice", "data1", "read"); err != nil { + t.Fatalf("Enforce failed: %v", err) + } else if !result { + t.Errorf("Expected alice to have read access to data1") + } + + if result, err := e.Enforce("bob", "data2", "write"); err != nil { + t.Fatalf("Enforce failed: %v", err) + } else if !result { + t.Errorf("Expected bob to have write access to data2") + } + + // Test AddPolicy event + if added, err := e.AddPolicy("charlie", "data3", "read"); err != nil { + t.Fatalf("AddPolicy failed: %v", err) + } else if !added { + t.Errorf("Expected policy to be added") + } + + // Test RemovePolicy event + if removed, err := e.RemovePolicy("charlie", "data3", "read"); err != nil { + t.Fatalf("RemovePolicy failed: %v", err) + } else if !removed { + t.Errorf("Expected policy to be removed") + } + + // Test SavePolicy and LoadPolicy events + if err := e.SavePolicy(); err != nil { + t.Fatalf("SavePolicy failed: %v", err) + } + if err := e.LoadPolicy(); err != nil { + t.Fatalf("LoadPolicy failed: %v", err) + } + + // Verify buffer output and callback entries + verifyBufferOutput(t, buf.String()) + + if len(callbackEntries) == 0 { + t.Fatalf("Expected callback to be called, but got no entries") + } + verifyCallbackEntries(t, callbackEntries) +} + +func TestSetEventTypes(t *testing.T) { + // Create enforcer with RBAC model and policy + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Create a buffer to capture log output + var buf bytes.Buffer + logger := log.NewDefaultLogger() + logger.SetOutput(&buf) + + // Set up a callback to track log entries + var callbackEntries []*log.LogEntry + err = logger.SetLogCallback(func(entry *log.LogEntry) error { + // Create a copy of the entry to store + entryCopy := *entry + callbackEntries = append(callbackEntries, &entryCopy) + return nil + }) + if err != nil { + t.Fatalf("Failed to set log callback: %v", err) + } + + // Configure logger to only log EventEnforce and EventAddPolicy + err = logger.SetEventTypes([]log.EventType{log.EventEnforce, log.EventAddPolicy}) + if err != nil { + t.Fatalf("Failed to set event types: %v", err) + } + + // Set the logger on the enforcer + e.SetLogger(logger) + + // Perform various operations + _, err = e.Enforce("alice", "data1", "read") + if err != nil { + t.Fatalf("Enforce failed: %v", err) + } + + _, err = e.AddPolicy("charlie", "data3", "read") + if err != nil { + t.Fatalf("AddPolicy failed: %v", err) + } + + _, err = e.RemovePolicy("charlie", "data3", "read") + if err != nil { + t.Fatalf("RemovePolicy failed: %v", err) + } + + if err := e.LoadPolicy(); err != nil { + t.Fatalf("LoadPolicy failed: %v", err) + } + + // Verify buffer output only contains EventEnforce and EventAddPolicy + verifySelectiveBufferOutput(t, buf.String()) + + // Verify callback entries + verifySelectiveCallbackEntries(t, callbackEntries) +} + +func verifySelectiveBufferOutput(t *testing.T, logOutput string) { + t.Helper() + if !strings.Contains(logOutput, "[enforce]") { + t.Errorf("Expected log output to contain enforce events") + } + if !strings.Contains(logOutput, "[addPolicy]") { + t.Errorf("Expected log output to contain addPolicy event") + } + if strings.Contains(logOutput, "[removePolicy]") { + t.Errorf("Did not expect log output to contain removePolicy event") + } + if strings.Contains(logOutput, "[loadPolicy]") { + t.Errorf("Did not expect log output to contain loadPolicy event") + } +} + +func verifySelectiveCallbackEntries(t *testing.T, entries []*log.LogEntry) { + t.Helper() + found := map[log.EventType]bool{} + + for _, entry := range entries { + found[entry.EventType] = true + checkEntryActiveStatus(t, entry) + } + + requiredEvents := []log.EventType{ + log.EventEnforce, log.EventAddPolicy, log.EventRemovePolicy, log.EventLoadPolicy, + } + for _, eventType := range requiredEvents { + if !found[eventType] { + t.Errorf("Expected to find %s in callback entries", eventType) + } + } +} + +func checkEntryActiveStatus(t *testing.T, entry *log.LogEntry) { + t.Helper() + switch entry.EventType { + case log.EventEnforce, log.EventAddPolicy: + if !entry.IsActive { + t.Errorf("Expected %s entry to be active", entry.EventType) + } + case log.EventRemovePolicy, log.EventLoadPolicy: + if entry.IsActive { + t.Errorf("Expected %s entry to be inactive", entry.EventType) + } + case log.EventSavePolicy: + // SavePolicy event exists but we're not checking it in this test + } +} diff --git a/management_api.go b/management_api.go new file mode 100644 index 0000000..0491ee5 --- /dev/null +++ b/management_api.go @@ -0,0 +1,519 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "errors" + "fmt" + "strings" + + "github.com/casbin/casbin/v3/constant" + "github.com/casbin/casbin/v3/util" + "github.com/casbin/govaluate" +) + +// GetAllSubjects gets the list of subjects that show up in the current policy. +func (e *Enforcer) GetAllSubjects() ([]string, error) { + return e.model.GetValuesForFieldInPolicyAllTypesByName("p", constant.SubjectIndex) +} + +// GetAllNamedSubjects gets the list of subjects that show up in the current named policy. +func (e *Enforcer) GetAllNamedSubjects(ptype string) ([]string, error) { + fieldIndex, err := e.model.GetFieldIndex(ptype, constant.SubjectIndex) + if err != nil { + return nil, err + } + return e.model.GetValuesForFieldInPolicy("p", ptype, fieldIndex) +} + +// GetAllObjects gets the list of objects that show up in the current policy. +func (e *Enforcer) GetAllObjects() ([]string, error) { + return e.model.GetValuesForFieldInPolicyAllTypesByName("p", constant.ObjectIndex) +} + +// GetAllNamedObjects gets the list of objects that show up in the current named policy. +func (e *Enforcer) GetAllNamedObjects(ptype string) ([]string, error) { + fieldIndex, err := e.model.GetFieldIndex(ptype, constant.ObjectIndex) + if err != nil { + return nil, err + } + return e.model.GetValuesForFieldInPolicy("p", ptype, fieldIndex) +} + +// GetAllActions gets the list of actions that show up in the current policy. +func (e *Enforcer) GetAllActions() ([]string, error) { + return e.model.GetValuesForFieldInPolicyAllTypesByName("p", constant.ActionIndex) +} + +// GetAllNamedActions gets the list of actions that show up in the current named policy. +func (e *Enforcer) GetAllNamedActions(ptype string) ([]string, error) { + fieldIndex, err := e.model.GetFieldIndex(ptype, constant.ActionIndex) + if err != nil { + return nil, err + } + return e.model.GetValuesForFieldInPolicy("p", ptype, fieldIndex) +} + +// GetAllRoles gets the list of roles that show up in the current policy. +func (e *Enforcer) GetAllRoles() ([]string, error) { + return e.model.GetValuesForFieldInPolicyAllTypes("g", 1) +} + +// GetAllNamedRoles gets the list of roles that show up in the current named policy. +func (e *Enforcer) GetAllNamedRoles(ptype string) ([]string, error) { + return e.model.GetValuesForFieldInPolicy("g", ptype, 1) +} + +// GetAllUsers gets the list of users that show up in the current policy. +// Users are subjects that are not roles (i.e., subjects that do not appear as the second element in any grouping policy). +func (e *Enforcer) GetAllUsers() ([]string, error) { + subjects, err := e.GetAllSubjects() + if err != nil { + return nil, err + } + + roles, err := e.GetAllRoles() + if err != nil { + return nil, err + } + + users := util.SetSubtract(subjects, roles) + return users, nil +} + +// GetPolicy gets all the authorization rules in the policy. +func (e *Enforcer) GetPolicy() ([][]string, error) { + return e.GetNamedPolicy("p") +} + +// GetFilteredPolicy gets all the authorization rules in the policy, field filters can be specified. +func (e *Enforcer) GetFilteredPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) { + return e.GetFilteredNamedPolicy("p", fieldIndex, fieldValues...) +} + +// GetNamedPolicy gets all the authorization rules in the named policy. +func (e *Enforcer) GetNamedPolicy(ptype string) ([][]string, error) { + return e.model.GetPolicy("p", ptype) +} + +// GetFilteredNamedPolicy gets all the authorization rules in the named policy, field filters can be specified. +func (e *Enforcer) GetFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) { + return e.model.GetFilteredPolicy("p", ptype, fieldIndex, fieldValues...) +} + +// GetGroupingPolicy gets all the role inheritance rules in the policy. +func (e *Enforcer) GetGroupingPolicy() ([][]string, error) { + return e.GetNamedGroupingPolicy("g") +} + +// GetFilteredGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified. +func (e *Enforcer) GetFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) ([][]string, error) { + return e.GetFilteredNamedGroupingPolicy("g", fieldIndex, fieldValues...) +} + +// GetNamedGroupingPolicy gets all the role inheritance rules in the policy. +func (e *Enforcer) GetNamedGroupingPolicy(ptype string) ([][]string, error) { + return e.model.GetPolicy("g", ptype) +} + +// GetFilteredNamedGroupingPolicy gets all the role inheritance rules in the policy, field filters can be specified. +func (e *Enforcer) GetFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) { + return e.model.GetFilteredPolicy("g", ptype, fieldIndex, fieldValues...) +} + +// GetFilteredNamedPolicyWithMatcher gets rules based on matcher from the policy. +func (e *Enforcer) GetFilteredNamedPolicyWithMatcher(ptype string, matcher string) ([][]string, error) { + var res [][]string + var err error + + functions := e.fm.GetFunctions() + if _, ok := e.model["g"]; ok { + for key, ast := range e.model["g"] { + // g must be a normal role definition (ast.RM != nil) + // or a conditional role definition (ast.CondRM != nil) + // ast.RM and ast.CondRM shouldn't be nil at the same time + if ast.RM != nil { + functions[key] = util.GenerateGFunction(ast.RM, e.gFunctionCache) + } + if ast.CondRM != nil { + functions[key] = util.GenerateConditionalGFunction(ast.CondRM) + } + } + } + + var expString string + if matcher == "" { + return res, fmt.Errorf("matcher is empty") + } else { + expString = util.RemoveComments(util.EscapeAssertion(matcher)) + } + + var expression *govaluate.EvaluableExpression + + expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions) + if err != nil { + return res, err + } + + pTokens := make(map[string]int, len(e.model["p"][ptype].Tokens)) + for i, token := range e.model["p"][ptype].Tokens { + pTokens[token] = i + } + + parameters := enforceParameters{ + pTokens: pTokens, + } + + if policyLen := len(e.model["p"][ptype].Policy); policyLen != 0 && strings.Contains(expString, ptype+"_") { + for _, pvals := range e.model["p"][ptype].Policy { + if len(e.model["p"][ptype].Tokens) != len(pvals) { + return res, fmt.Errorf( + "invalid policy size: expected %d, got %d, pvals: %v", + len(e.model["p"][ptype].Tokens), + len(pvals), + pvals) + } + + parameters.pVals = pvals + + result, err := expression.Eval(parameters) + if err != nil { + return res, err + } + + switch result := result.(type) { + case bool: + if result { + res = append(res, pvals) + } + case float64: + if result != 0 { + res = append(res, pvals) + } + default: + return res, errors.New("matcher result should be bool, int or float") + } + } + } + return res, nil +} + +// HasPolicy determines whether an authorization rule exists. +func (e *Enforcer) HasPolicy(params ...interface{}) (bool, error) { + return e.HasNamedPolicy("p", params...) +} + +// HasNamedPolicy determines whether a named authorization rule exists. +func (e *Enforcer) HasNamedPolicy(ptype string, params ...interface{}) (bool, error) { + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + return e.model.HasPolicy("p", ptype, strSlice) + } + + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.model.HasPolicy("p", ptype, policy) +} + +// AddPolicy adds an authorization rule to the current policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *Enforcer) AddPolicy(params ...interface{}) (bool, error) { + return e.AddNamedPolicy("p", params...) +} + +// AddPolicies adds authorization rules to the current policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding rule by adding the new rule. +func (e *Enforcer) AddPolicies(rules [][]string) (bool, error) { + return e.AddNamedPolicies("p", rules) +} + +// AddPoliciesEx adds authorization rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddPolicies, other non-existent rules are added instead of returning false directly. +func (e *Enforcer) AddPoliciesEx(rules [][]string) (bool, error) { + return e.AddNamedPoliciesEx("p", rules) +} + +// AddNamedPolicy adds an authorization rule to the current named policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *Enforcer) AddNamedPolicy(ptype string, params ...interface{}) (bool, error) { + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + strSlice = append(make([]string, 0, len(strSlice)), strSlice...) + return e.addPolicy("p", ptype, strSlice) + } + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.addPolicy("p", ptype, policy) +} + +// AddNamedPolicies adds authorization rules to the current named policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding by adding the new rule. +func (e *Enforcer) AddNamedPolicies(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("p", ptype, rules, false) +} + +// AddNamedPoliciesEx adds authorization rules to the current named policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedPolicies, other non-existent rules are added instead of returning false directly. +func (e *Enforcer) AddNamedPoliciesEx(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("p", ptype, rules, true) +} + +// RemovePolicy removes an authorization rule from the current policy. +func (e *Enforcer) RemovePolicy(params ...interface{}) (bool, error) { + return e.RemoveNamedPolicy("p", params...) +} + +// UpdatePolicy updates an authorization rule from the current policy. +func (e *Enforcer) UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error) { + return e.UpdateNamedPolicy("p", oldPolicy, newPolicy) +} + +func (e *Enforcer) UpdateNamedPolicy(ptype string, p1 []string, p2 []string) (bool, error) { + return e.updatePolicy("p", ptype, p1, p2) +} + +// UpdatePolicies updates authorization rules from the current policies. +func (e *Enforcer) UpdatePolicies(oldPolices [][]string, newPolicies [][]string) (bool, error) { + return e.UpdateNamedPolicies("p", oldPolices, newPolicies) +} + +func (e *Enforcer) UpdateNamedPolicies(ptype string, p1 [][]string, p2 [][]string) (bool, error) { + return e.updatePolicies("p", ptype, p1, p2) +} + +func (e *Enforcer) UpdateFilteredPolicies(newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.UpdateFilteredNamedPolicies("p", newPolicies, fieldIndex, fieldValues...) +} + +func (e *Enforcer) UpdateFilteredNamedPolicies(ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.updateFilteredPolicies("p", ptype, newPolicies, fieldIndex, fieldValues...) +} + +// RemovePolicies removes authorization rules from the current policy. +func (e *Enforcer) RemovePolicies(rules [][]string) (bool, error) { + return e.RemoveNamedPolicies("p", rules) +} + +// RemoveFilteredPolicy removes an authorization rule from the current policy, field filters can be specified. +func (e *Enforcer) RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error) { + return e.RemoveFilteredNamedPolicy("p", fieldIndex, fieldValues...) +} + +// RemoveNamedPolicy removes an authorization rule from the current named policy. +func (e *Enforcer) RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error) { + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + return e.removePolicy("p", ptype, strSlice) + } + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.removePolicy("p", ptype, policy) +} + +// RemoveNamedPolicies removes authorization rules from the current named policy. +func (e *Enforcer) RemoveNamedPolicies(ptype string, rules [][]string) (bool, error) { + return e.removePolicies("p", ptype, rules) +} + +// RemoveFilteredNamedPolicy removes an authorization rule from the current named policy, field filters can be specified. +func (e *Enforcer) RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.removeFilteredPolicy("p", ptype, fieldIndex, fieldValues) +} + +// HasGroupingPolicy determines whether a role inheritance rule exists. +func (e *Enforcer) HasGroupingPolicy(params ...interface{}) (bool, error) { + return e.HasNamedGroupingPolicy("g", params...) +} + +// HasNamedGroupingPolicy determines whether a named role inheritance rule exists. +func (e *Enforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + return e.model.HasPolicy("g", ptype, strSlice) + } + + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.model.HasPolicy("g", ptype, policy) +} + +// AddGroupingPolicy adds a role inheritance rule to the current policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *Enforcer) AddGroupingPolicy(params ...interface{}) (bool, error) { + return e.AddNamedGroupingPolicy("g", params...) +} + +// AddGroupingPolicies adds role inheritance rules to the current policy. +// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. +// Otherwise the function returns true for the corresponding policy rule by adding the new rule. +func (e *Enforcer) AddGroupingPolicies(rules [][]string) (bool, error) { + return e.AddNamedGroupingPolicies("g", rules) +} + +// AddGroupingPoliciesEx adds role inheritance rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddGroupingPolicies, other non-existent rules are added instead of returning false directly. +func (e *Enforcer) AddGroupingPoliciesEx(rules [][]string) (bool, error) { + return e.AddNamedGroupingPoliciesEx("g", rules) +} + +// AddNamedGroupingPolicy adds a named role inheritance rule to the current policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *Enforcer) AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + var ruleAdded bool + var err error + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + ruleAdded, err = e.addPolicy("g", ptype, strSlice) + } else { + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + ruleAdded, err = e.addPolicy("g", ptype, policy) + } + + return ruleAdded, err +} + +// AddNamedGroupingPolicies adds named role inheritance rules to the current policy. +// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. +// Otherwise the function returns true for the corresponding policy rule by adding the new rule. +func (e *Enforcer) AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("g", ptype, rules, false) +} + +// AddNamedGroupingPoliciesEx adds named role inheritance rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedGroupingPolicies, other non-existent rules are added instead of returning false directly. +func (e *Enforcer) AddNamedGroupingPoliciesEx(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("g", ptype, rules, true) +} + +// RemoveGroupingPolicy removes a role inheritance rule from the current policy. +func (e *Enforcer) RemoveGroupingPolicy(params ...interface{}) (bool, error) { + return e.RemoveNamedGroupingPolicy("g", params...) +} + +// RemoveGroupingPolicies removes role inheritance rules from the current policy. +func (e *Enforcer) RemoveGroupingPolicies(rules [][]string) (bool, error) { + return e.RemoveNamedGroupingPolicies("g", rules) +} + +// RemoveFilteredGroupingPolicy removes a role inheritance rule from the current policy, field filters can be specified. +func (e *Enforcer) RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error) { + return e.RemoveFilteredNamedGroupingPolicy("g", fieldIndex, fieldValues...) +} + +// RemoveNamedGroupingPolicy removes a role inheritance rule from the current named policy. +func (e *Enforcer) RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + var ruleRemoved bool + var err error + if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + ruleRemoved, err = e.removePolicy("g", ptype, strSlice) + } else { + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + ruleRemoved, err = e.removePolicy("g", ptype, policy) + } + + return ruleRemoved, err +} + +// RemoveNamedGroupingPolicies removes role inheritance rules from the current named policy. +func (e *Enforcer) RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + return e.removePolicies("g", ptype, rules) +} + +func (e *Enforcer) UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) { + return e.UpdateNamedGroupingPolicy("g", oldRule, newRule) +} + +// UpdateGroupingPolicies updates authorization rules from the current policies. +func (e *Enforcer) UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error) { + return e.UpdateNamedGroupingPolicies("g", oldRules, newRules) +} + +func (e *Enforcer) UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error) { + return e.updatePolicy("g", ptype, oldRule, newRule) +} + +func (e *Enforcer) UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + return e.updatePolicies("g", ptype, oldRules, newRules) +} + +// RemoveFilteredNamedGroupingPolicy removes a role inheritance rule from the current named policy, field filters can be specified. +func (e *Enforcer) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.removeFilteredPolicy("g", ptype, fieldIndex, fieldValues) +} + +// AddFunction adds a customized function. +func (e *Enforcer) AddFunction(name string, function govaluate.ExpressionFunction) { + e.fm.AddFunction(name, function) +} + +func (e *Enforcer) SelfAddPolicy(sec string, ptype string, rule []string) (bool, error) { + return e.addPolicyWithoutNotify(sec, ptype, rule) +} + +func (e *Enforcer) SelfAddPolicies(sec string, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesWithoutNotify(sec, ptype, rules, false) +} + +func (e *Enforcer) SelfAddPoliciesEx(sec string, ptype string, rules [][]string) (bool, error) { + return e.addPoliciesWithoutNotify(sec, ptype, rules, true) +} + +func (e *Enforcer) SelfRemovePolicy(sec string, ptype string, rule []string) (bool, error) { + return e.removePolicyWithoutNotify(sec, ptype, rule) +} + +func (e *Enforcer) SelfRemovePolicies(sec string, ptype string, rules [][]string) (bool, error) { + return e.removePoliciesWithoutNotify(sec, ptype, rules) +} + +func (e *Enforcer) SelfRemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.removeFilteredPolicyWithoutNotify(sec, ptype, fieldIndex, fieldValues) +} + +func (e *Enforcer) SelfUpdatePolicy(sec string, ptype string, oldRule, newRule []string) (bool, error) { + return e.updatePolicyWithoutNotify(sec, ptype, oldRule, newRule) +} + +func (e *Enforcer) SelfUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) { + return e.updatePoliciesWithoutNotify(sec, ptype, oldRules, newRules) +} diff --git a/management_api_b_test.go b/management_api_b_test.go new file mode 100644 index 0000000..b4e7e88 --- /dev/null +++ b/management_api_b_test.go @@ -0,0 +1,225 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + "math/rand" + "testing" +) + +func BenchmarkHasPolicySmall(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 100 roles, 10 resources. + for i := 0; i < 100; i++ { + _, _ = e.AddPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + e.HasPolicy(fmt.Sprintf("user%d", rand.Intn(100)), fmt.Sprintf("data%d", rand.Intn(100)/10), "read") + } +} + +func BenchmarkHasPolicyMedium(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 1000 roles, 100 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 1000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + e.HasPolicy(fmt.Sprintf("user%d", rand.Intn(1000)), fmt.Sprintf("data%d", rand.Intn(1000)/10), "read") + } +} + +func BenchmarkHasPolicyLarge(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + e.HasPolicy(fmt.Sprintf("user%d", rand.Intn(10000)), fmt.Sprintf("data%d", rand.Intn(10000)/10), "read") + } +} + +func BenchmarkAddPolicySmall(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 100 roles, 10 resources. + for i := 0; i < 100; i++ { + _, _ = e.AddPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.AddPolicy(fmt.Sprintf("user%d", rand.Intn(100)+100), fmt.Sprintf("data%d", (rand.Intn(100)+100)/10), "read") + } +} + +func BenchmarkAddPolicyMedium(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 1000 roles, 100 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 1000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.AddPolicy(fmt.Sprintf("user%d", rand.Intn(1000)+1000), fmt.Sprintf("data%d", (rand.Intn(1000)+1000)/10), "read") + } +} + +func BenchmarkAddPolicyLarge(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.AddPolicy(fmt.Sprintf("user%d", rand.Intn(10000)+10000), fmt.Sprintf("data%d", (rand.Intn(10000)+10000)/10), "read") + } +} + +func BenchmarkRemovePolicySmall(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 100 roles, 10 resources. + for i := 0; i < 100; i++ { + _, _ = e.AddPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.RemovePolicy(fmt.Sprintf("user%d", rand.Intn(100)), fmt.Sprintf("data%d", rand.Intn(100)/10), "read") + } +} + +func BenchmarkRemovePolicyMedium(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 1000 roles, 100 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 1000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.RemovePolicy(fmt.Sprintf("user%d", rand.Intn(1000)), fmt.Sprintf("data%d", rand.Intn(1000)/10), "read") + } +} + +func BenchmarkRemovePolicyLarge(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.RemovePolicy(fmt.Sprintf("user%d", rand.Intn(10000)), fmt.Sprintf("data%d", rand.Intn(10000)/10), "read") + } +} + +func BenchmarkRemovePoliciesLarge(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // Remove 100 rules per batch, restoring them afterwards so that every + // iteration runs against the same 10000-rule policy set. Batches are taken + // in order, and restored rules go to the end of the policy list, so the + // next batch is always at the front - the worst case for index rebuilding. + const batchSize = 100 + batches := make([][][]string, 0, len(pPolicies)/batchSize) + for i := 0; i < len(pPolicies); i += batchSize { + batches = append(batches, pPolicies[i:i+batchSize]) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + rules := batches[i%len(batches)] + + removed, err := e.RemovePolicies(rules) + if err != nil { + b.Fatal(err) + } + if !removed { + b.Fatal("expected RemovePolicies to remove rules") + } + + b.StopTimer() + added, err := e.AddPolicies(rules) + if err != nil { + b.Fatal(err) + } + if !added { + b.Fatal("expected AddPolicies to add rules") + } + b.StartTimer() + } +} diff --git a/management_api_test.go b/management_api_test.go new file mode 100644 index 0000000..b04fba1 --- /dev/null +++ b/management_api_test.go @@ -0,0 +1,372 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" + + "github.com/casbin/casbin/v3/util" +) + +func testStringList(t *testing.T, title string, f func() ([]string, error), res []string) { + t.Helper() + myRes, err := f() + if err != nil { + t.Error(err) + } + + t.Log(title+": ", myRes) + + if !util.ArrayEquals(res, myRes) { + t.Error(title+": ", myRes, ", supposed to be ", res) + } +} + +func TestGetList(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testStringList(t, "Subjects", e.GetAllSubjects, []string{"alice", "bob", "data2_admin"}) + testStringList(t, "Objects", e.GetAllObjects, []string{"data1", "data2"}) + testStringList(t, "Actions", e.GetAllActions, []string{"read", "write"}) + testStringList(t, "Roles", e.GetAllRoles, []string{"data2_admin"}) + testStringList(t, "Users", e.GetAllUsers, []string{"alice", "bob"}) +} + +func TestGetListWithDomains(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testStringList(t, "Subjects", e.GetAllSubjects, []string{"admin"}) + testStringList(t, "Objects", e.GetAllObjects, []string{"data1", "data2"}) + testStringList(t, "Actions", e.GetAllActions, []string{"read", "write"}) + testStringList(t, "Roles", e.GetAllRoles, []string{"admin"}) + testStringList(t, "Users", e.GetAllUsers, []string{}) +} + +func testGetPolicy(t *testing.T, e *Enforcer, res [][]string) { + t.Helper() + myRes, err := e.GetPolicy() + if err != nil { + t.Error(err) + } + + t.Log("Policy: ", myRes) + + if !util.SortedArray2DEquals(res, myRes) { + t.Error("Policy: ", myRes, ", supposed to be ", res) + } +} + +func testGetFilteredPolicy(t *testing.T, e *Enforcer, fieldIndex int, res [][]string, fieldValues ...string) { + t.Helper() + myRes, err := e.GetFilteredPolicy(fieldIndex, fieldValues...) + if err != nil { + t.Error(err) + } + + t.Log("Policy for ", util.ParamsToString(fieldValues...), ": ", myRes) + + if !util.Array2DEquals(res, myRes) { + t.Error("Policy for ", util.ParamsToString(fieldValues...), ": ", myRes, ", supposed to be ", res) + } +} + +func testGetFilteredNamedPolicyWithMatcher(t *testing.T, e *Enforcer, ptype string, matcher string, res [][]string) { + t.Helper() + myRes, err := e.GetFilteredNamedPolicyWithMatcher(ptype, matcher) + t.Log("Policy for", matcher, ": ", myRes) + + if err != nil { + t.Error(err) + } + + if !util.Array2DEquals(res, myRes) { + t.Error("Policy for ", matcher, ": ", myRes, ", supposed to be ", res) + } +} + +func testGetGroupingPolicy(t *testing.T, e *Enforcer, res [][]string) { + t.Helper() + myRes, err := e.GetGroupingPolicy() + if err != nil { + t.Error(err) + } + + t.Log("Grouping policy: ", myRes) + + if !util.Array2DEquals(res, myRes) { + t.Error("Grouping policy: ", myRes, ", supposed to be ", res) + } +} + +func testGetFilteredGroupingPolicy(t *testing.T, e *Enforcer, fieldIndex int, res [][]string, fieldValues ...string) { + t.Helper() + myRes, err := e.GetFilteredGroupingPolicy(fieldIndex, fieldValues...) + if err != nil { + t.Error(err) + } + + t.Log("Grouping policy for ", util.ParamsToString(fieldValues...), ": ", myRes) + + if !util.Array2DEquals(res, myRes) { + t.Error("Grouping policy for ", util.ParamsToString(fieldValues...), ": ", myRes, ", supposed to be ", res) + } +} + +func testHasPolicy(t *testing.T, e *Enforcer, policy []string, res bool) { + t.Helper() + myRes, err := e.HasPolicy(policy) + if err != nil { + t.Error(err) + } + + t.Log("Has policy ", util.ArrayToString(policy), ": ", myRes) + + if res != myRes { + t.Error("Has policy ", util.ArrayToString(policy), ": ", myRes, ", supposed to be ", res) + } +} + +func testHasGroupingPolicy(t *testing.T, e *Enforcer, policy []string, res bool) { + t.Helper() + myRes, err := e.HasGroupingPolicy(policy) + if err != nil { + t.Error(err) + } + + t.Log("Has grouping policy ", util.ArrayToString(policy), ": ", myRes) + + if res != myRes { + t.Error("Has grouping policy ", util.ArrayToString(policy), ": ", myRes, ", supposed to be ", res) + } +} + +func TestGetPolicyAPI(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"data2_admin", "data2", "read"}, + {"data2_admin", "data2", "write"}, + }) + + testGetFilteredPolicy(t, e, 0, [][]string{{"alice", "data1", "read"}}, "alice") + testGetFilteredPolicy(t, e, 0, [][]string{{"bob", "data2", "write"}}, "bob") + testGetFilteredPolicy(t, e, 0, [][]string{{"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}, "data2_admin") + testGetFilteredPolicy(t, e, 1, [][]string{{"alice", "data1", "read"}}, "data1") + testGetFilteredPolicy(t, e, 1, [][]string{{"bob", "data2", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}, "data2") + testGetFilteredPolicy(t, e, 2, [][]string{{"alice", "data1", "read"}, {"data2_admin", "data2", "read"}}, "read") + testGetFilteredPolicy(t, e, 2, [][]string{{"bob", "data2", "write"}, {"data2_admin", "data2", "write"}}, "write") + + testGetFilteredNamedPolicyWithMatcher(t, e, "p", "'alice' == p.sub", [][]string{{"alice", "data1", "read"}}) + testGetFilteredNamedPolicyWithMatcher(t, e, "p", "keyMatch2(p.sub, '*')", [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"data2_admin", "data2", "read"}, + {"data2_admin", "data2", "write"}, + }) + + testGetFilteredPolicy(t, e, 0, [][]string{{"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}, "data2_admin", "data2") + // Note: "" (empty string) in fieldValues means matching all values. + testGetFilteredPolicy(t, e, 0, [][]string{{"data2_admin", "data2", "read"}}, "data2_admin", "", "read") + testGetFilteredPolicy(t, e, 1, [][]string{{"bob", "data2", "write"}, {"data2_admin", "data2", "write"}}, "data2", "write") + + testHasPolicy(t, e, []string{"alice", "data1", "read"}, true) + testHasPolicy(t, e, []string{"bob", "data2", "write"}, true) + testHasPolicy(t, e, []string{"alice", "data2", "read"}, false) + testHasPolicy(t, e, []string{"bob", "data3", "write"}, false) + + testGetGroupingPolicy(t, e, [][]string{{"alice", "data2_admin"}}) + + testGetFilteredGroupingPolicy(t, e, 0, [][]string{{"alice", "data2_admin"}}, "alice") + testGetFilteredGroupingPolicy(t, e, 0, [][]string{}, "bob") + testGetFilteredGroupingPolicy(t, e, 1, [][]string{}, "data1_admin") + testGetFilteredGroupingPolicy(t, e, 1, [][]string{{"alice", "data2_admin"}}, "data2_admin") + // Note: "" (empty string) in fieldValues means matching all values. + testGetFilteredGroupingPolicy(t, e, 0, [][]string{{"alice", "data2_admin"}}, "", "data2_admin") + + testHasGroupingPolicy(t, e, []string{"alice", "data2_admin"}, true) + testHasGroupingPolicy(t, e, []string{"bob", "data2_admin"}, false) +} + +func TestModifyPolicyAPI(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testGetPolicy(t, e, [][]string{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + {"data2_admin", "data2", "read"}, + {"data2_admin", "data2", "write"}, + }) + + _, _ = e.RemovePolicy("alice", "data1", "read") + _, _ = e.RemovePolicy("bob", "data2", "write") + _, _ = e.RemovePolicy("alice", "data1", "read") + _, _ = e.AddPolicy("eve", "data3", "read") + _, _ = e.AddPolicy("eve", "data3", "read") + + rules := [][]string{ + {"jack", "data4", "read"}, + {"jack", "data4", "read"}, + {"jack", "data4", "read"}, + {"katy", "data4", "write"}, + {"leyo", "data4", "read"}, + {"katy", "data4", "write"}, + {"katy", "data4", "write"}, + {"ham", "data4", "write"}, + } + + _, _ = e.AddPolicies(rules) + _, _ = e.AddPolicies(rules) + + testGetPolicy(t, e, [][]string{ + {"data2_admin", "data2", "read"}, + {"data2_admin", "data2", "write"}, + {"eve", "data3", "read"}, + {"jack", "data4", "read"}, + {"katy", "data4", "write"}, + {"leyo", "data4", "read"}, + {"ham", "data4", "write"}, + }) + + _, _ = e.RemovePolicies(rules) + _, _ = e.RemovePolicies(rules) + + namedPolicy := []string{"eve", "data3", "read"} + _, _ = e.RemoveNamedPolicy("p", namedPolicy) + _, _ = e.AddNamedPolicy("p", namedPolicy) + + testGetPolicy(t, e, [][]string{ + {"data2_admin", "data2", "read"}, + {"data2_admin", "data2", "write"}, + {"eve", "data3", "read"}, + }) + + _, _ = e.RemoveFilteredPolicy(1, "data2") + + testGetPolicy(t, e, [][]string{{"eve", "data3", "read"}}) + + _, _ = e.UpdatePolicy([]string{"eve", "data3", "read"}, []string{"eve", "data3", "write"}) + + testGetPolicy(t, e, [][]string{{"eve", "data3", "write"}}) + + // This test shows a rollback effect. + // _, _ = e.UpdatePolicies([][]string{{"eve", "data3", "write"}, {"jack", "data4", "read"}}, [][]string{{"eve", "data3", "read"}, {"jack", "data4", "write"}}) + // testGetPolicy(t, e, [][]string{{"eve", "data3", "read"}, {"jack", "data4", "write"}}) + + _, _ = e.AddPolicies(rules) + _, _ = e.UpdatePolicies([][]string{{"eve", "data3", "write"}, {"leyo", "data4", "read"}, {"katy", "data4", "write"}}, + [][]string{{"eve", "data3", "read"}, {"leyo", "data4", "write"}, {"katy", "data1", "write"}}) + testGetPolicy(t, e, [][]string{{"eve", "data3", "read"}, {"jack", "data4", "read"}, {"katy", "data1", "write"}, {"leyo", "data4", "write"}, {"ham", "data4", "write"}}) + + e.ClearPolicy() + _, _ = e.AddPoliciesEx([][]string{{"user1", "data1", "read"}, {"user1", "data1", "read"}}) + testGetPolicy(t, e, [][]string{{"user1", "data1", "read"}}) + // {"user1", "data1", "read"} repeated + _, _ = e.AddPoliciesEx([][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) + testGetPolicy(t, e, [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}}) + // {"user1", "data1", "read"}, {"user2", "data2", "read"} repeated + _, _ = e.AddNamedPoliciesEx("p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}, {"user3", "data3", "read"}}) + testGetPolicy(t, e, [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}, {"user3", "data3", "read"}}) + // {"user1", "data1", "read"}, {"user2", "data2", "read"}, , {"user3", "data3", "read"} repeated + _, _ = e.SelfAddPoliciesEx("p", "p", [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}, {"user3", "data3", "read"}, {"user4", "data4", "read"}}) + testGetPolicy(t, e, [][]string{{"user1", "data1", "read"}, {"user2", "data2", "read"}, {"user3", "data3", "read"}, {"user4", "data4", "read"}}) +} + +func TestModifyGroupingPolicyAPI(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testGetRoles(t, e, []string{"data2_admin"}, "alice") + testGetRoles(t, e, []string{}, "bob") + testGetRoles(t, e, []string{}, "eve") + testGetRoles(t, e, []string{}, "non_exist") + + _, _ = e.RemoveGroupingPolicy("alice", "data2_admin") + _, _ = e.AddGroupingPolicy("bob", "data1_admin") + _, _ = e.AddGroupingPolicy("eve", "data3_admin") + + groupingRules := [][]string{ + {"ham", "data4_admin"}, + {"jack", "data5_admin"}, + } + + _, _ = e.AddGroupingPolicies(groupingRules) + testGetRoles(t, e, []string{"data4_admin"}, "ham") + testGetRoles(t, e, []string{"data5_admin"}, "jack") + _, _ = e.RemoveGroupingPolicies(groupingRules) + + testGetRoles(t, e, []string{}, "alice") + namedGroupingPolicy := []string{"alice", "data2_admin"} + testGetRoles(t, e, []string{}, "alice") + _, _ = e.AddNamedGroupingPolicy("g", namedGroupingPolicy) + testGetRoles(t, e, []string{"data2_admin"}, "alice") + _, _ = e.RemoveNamedGroupingPolicy("g", namedGroupingPolicy) + + _, _ = e.AddNamedGroupingPolicies("g", groupingRules) + _, _ = e.AddNamedGroupingPolicies("g", groupingRules) + testGetRoles(t, e, []string{"data4_admin"}, "ham") + testGetRoles(t, e, []string{"data5_admin"}, "jack") + _, _ = e.RemoveNamedGroupingPolicies("g", groupingRules) + _, _ = e.RemoveNamedGroupingPolicies("g", groupingRules) + + testGetRoles(t, e, []string{}, "alice") + testGetRoles(t, e, []string{"data1_admin"}, "bob") + testGetRoles(t, e, []string{"data3_admin"}, "eve") + testGetRoles(t, e, []string{}, "non_exist") + + testGetUsers(t, e, []string{"bob"}, "data1_admin") + testGetUsers(t, e, []string{}, "data2_admin") + testGetUsers(t, e, []string{"eve"}, "data3_admin") + + _, _ = e.RemoveFilteredGroupingPolicy(0, "bob") + + testGetRoles(t, e, []string{}, "alice") + testGetRoles(t, e, []string{}, "bob") + testGetRoles(t, e, []string{"data3_admin"}, "eve") + testGetRoles(t, e, []string{}, "non_exist") + + testGetUsers(t, e, []string{}, "data1_admin") + testGetUsers(t, e, []string{}, "data2_admin") + testGetUsers(t, e, []string{"eve"}, "data3_admin") + _, _ = e.AddGroupingPolicy("data3_admin", "data4_admin") + _, _ = e.UpdateGroupingPolicy([]string{"eve", "data3_admin"}, []string{"eve", "admin"}) + _, _ = e.UpdateGroupingPolicy([]string{"data3_admin", "data4_admin"}, []string{"admin", "data4_admin"}) + testGetUsers(t, e, []string{"admin"}, "data4_admin") + testGetUsers(t, e, []string{"eve"}, "admin") + + testGetRoles(t, e, []string{"admin"}, "eve") + testGetRoles(t, e, []string{"data4_admin"}, "admin") + + _, _ = e.UpdateGroupingPolicies([][]string{{"eve", "admin"}}, [][]string{{"eve", "admin_groups"}}) + _, _ = e.UpdateGroupingPolicies([][]string{{"admin", "data4_admin"}}, [][]string{{"admin", "data5_admin"}}) + testGetUsers(t, e, []string{"admin"}, "data5_admin") + testGetUsers(t, e, []string{"eve"}, "admin_groups") + + testGetRoles(t, e, []string{"data5_admin"}, "admin") + testGetRoles(t, e, []string{"admin_groups"}, "eve") + + e.ClearPolicy() + _, _ = e.AddGroupingPoliciesEx([][]string{{"user1", "member"}}) + testGetUsers(t, e, []string{"user1"}, "member") + // {"user1", "member"} repeated + _, _ = e.AddGroupingPoliciesEx([][]string{{"user1", "member"}, {"user2", "member"}}) + testGetUsers(t, e, []string{"user1", "user2"}, "member") + // {"user1", "member"}, {"user2", "member"} repeated + _, _ = e.AddNamedGroupingPoliciesEx("g", [][]string{{"user1", "member"}, {"user2", "member"}, {"user3", "member"}}) + testGetUsers(t, e, []string{"user1", "user2", "user3"}, "member") +} diff --git a/model/assertion.go b/model/assertion.go new file mode 100644 index 0000000..3766ec4 --- /dev/null +++ b/model/assertion.go @@ -0,0 +1,202 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package model + +import ( + "errors" + "strings" + "sync" + + "github.com/casbin/casbin/v3/rbac" +) + +// Assertion represents an expression in a section of the model. +// For example: r = sub, obj, act. +type Assertion struct { + Key string + Value string + Tokens []string + ParamsTokens []string + Policy [][]string + PolicyMap map[string]int + RM rbac.RoleManager + CondRM rbac.ConditionalRoleManager + FieldIndexMap map[string]int + FieldIndexMutex sync.RWMutex +} + +func (ast *Assertion) buildIncrementalRoleLinks(rm rbac.RoleManager, op PolicyOp, rules [][]string) error { + ast.RM = rm + count := strings.Count(ast.Value, "_") + if count < 2 { + return errors.New("the number of \"_\" in role definition should be at least 2") + } + + for _, rule := range rules { + if len(rule) < count { + return errors.New("grouping policy elements do not meet role definition") + } + if len(rule) > count { + rule = rule[:count] + } + switch op { + case PolicyAdd: + err := rm.AddLink(rule[0], rule[1], rule[2:]...) + if err != nil { + return err + } + case PolicyRemove: + err := rm.DeleteLink(rule[0], rule[1], rule[2:]...) + if err != nil { + return err + } + } + } + return nil +} + +func (ast *Assertion) buildRoleLinks(rm rbac.RoleManager) error { + ast.RM = rm + count := strings.Count(ast.Value, "_") + if count < 2 { + return errors.New("the number of \"_\" in role definition should be at least 2") + } + for _, rule := range ast.Policy { + if len(rule) < count { + return errors.New("grouping policy elements do not meet role definition") + } + if len(rule) > count { + rule = rule[:count] + } + err := ast.RM.AddLink(rule[0], rule[1], rule[2:]...) + if err != nil { + return err + } + } + + return nil +} + +func (ast *Assertion) buildIncrementalConditionalRoleLinks(condRM rbac.ConditionalRoleManager, op PolicyOp, rules [][]string) error { + ast.CondRM = condRM + count := strings.Count(ast.Value, "_") + if count < 2 { + return errors.New("the number of \"_\" in role definition should be at least 2") + } + + for _, rule := range rules { + if len(rule) < count { + return errors.New("grouping policy elements do not meet role definition") + } + if len(rule) > count { + rule = rule[:count] + } + + var err error + domainRule := rule[2:len(ast.Tokens)] + + switch op { + case PolicyAdd: + err = ast.addConditionalRoleLink(rule, domainRule) + case PolicyRemove: + err = ast.CondRM.DeleteLink(rule[0], rule[1], rule[2:]...) + } + if err != nil { + return err + } + } + + return nil +} + +func (ast *Assertion) buildConditionalRoleLinks(condRM rbac.ConditionalRoleManager) error { + ast.CondRM = condRM + count := strings.Count(ast.Value, "_") + if count < 2 { + return errors.New("the number of \"_\" in role definition should be at least 2") + } + for _, rule := range ast.Policy { + if len(rule) < count { + return errors.New("grouping policy elements do not meet role definition") + } + if len(rule) > count { + rule = rule[:count] + } + + domainRule := rule[2:len(ast.Tokens)] + + err := ast.addConditionalRoleLink(rule, domainRule) + if err != nil { + return err + } + } + + return nil +} + +// addConditionalRoleLink adds Link to rbac.ConditionalRoleManager and sets the parameters for LinkConditionFunc. +func (ast *Assertion) addConditionalRoleLink(rule []string, domainRule []string) error { + var err error + if len(domainRule) == 0 { + err = ast.CondRM.AddLink(rule[0], rule[1]) + if err == nil { + ast.CondRM.SetLinkConditionFuncParams(rule[0], rule[1], rule[len(ast.Tokens):]...) + } + } else { + domain := domainRule[0] + err = ast.CondRM.AddLink(rule[0], rule[1], domain) + if err == nil { + ast.CondRM.SetDomainLinkConditionFuncParams(rule[0], rule[1], domain, rule[len(ast.Tokens):]...) + } + } + return err +} + +func (ast *Assertion) copy() *Assertion { + tokens := append([]string(nil), ast.Tokens...) + policy := make([][]string, len(ast.Policy)) + + for i, p := range ast.Policy { + policy[i] = append(policy[i], p...) + } + policyMap := make(map[string]int) + for k, v := range ast.PolicyMap { + policyMap[k] = v + } + + ast.FieldIndexMutex.RLock() + fieldIndexMap := make(map[string]int) + for k, v := range ast.FieldIndexMap { + fieldIndexMap[k] = v + } + ast.FieldIndexMutex.RUnlock() + + newAst := &Assertion{ + Key: ast.Key, + Value: ast.Value, + PolicyMap: policyMap, + Tokens: tokens, + Policy: policy, + FieldIndexMap: fieldIndexMap, + ParamsTokens: append([]string(nil), ast.ParamsTokens...), + RM: ast.RM, + CondRM: ast.CondRM, + } + + return newAst +} diff --git a/model/constraint.go b/model/constraint.go new file mode 100644 index 0000000..bebf5b6 --- /dev/null +++ b/model/constraint.go @@ -0,0 +1,285 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package model + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/casbin/casbin/v3/errors" +) + +// ConstraintType represents the type of constraint. +type ConstraintType int + +const ( + ConstraintTypeSOD ConstraintType = iota + ConstraintTypeSODMax + ConstraintTypeRoleMax + ConstraintTypeRolePre +) + +// Constraint represents a policy constraint. +type Constraint struct { + Key string + Type ConstraintType + Roles []string + Role string + MaxCount int + PreReqRole string +} + +var ( + // Regex patterns for parsing constraints (compiled once at package initialization). + sodPattern = regexp.MustCompile(`^sod\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$`) + sodMaxPattern = regexp.MustCompile(`^sodMax\s*\(\s*\[([^\]]+)\]\s*,\s*(\d+)\s*\)$`) + roleMaxPattern = regexp.MustCompile(`^roleMax\s*\(\s*"([^"]+)"\s*,\s*(\d+)\s*\)$`) + rolePrePattern = regexp.MustCompile(`^rolePre\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$`) +) + +// parseRolesArray parses a comma-separated string of quoted role names. +func parseRolesArray(rolesStr string) ([]string, error) { + var roles []string + for _, role := range strings.Split(rolesStr, ",") { + role = strings.TrimSpace(role) + role = strings.Trim(role, `"`) + if role != "" { + roles = append(roles, role) + } + } + + if len(roles) == 0 { + return nil, fmt.Errorf("no roles found in role array") + } + + return roles, nil +} + +// parseConstraint parses a constraint definition string. +func parseConstraint(key, value string) (*Constraint, error) { + value = strings.TrimSpace(value) + + // Try to match sod pattern + if matches := sodPattern.FindStringSubmatch(value); matches != nil { + return &Constraint{ + Key: key, + Type: ConstraintTypeSOD, + Roles: []string{matches[1], matches[2]}, + }, nil + } + + // Try to match sodMax pattern + if matches := sodMaxPattern.FindStringSubmatch(value); matches != nil { + maxCount, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, fmt.Errorf("invalid max count in sodMax: %w", err) + } + + roles, err := parseRolesArray(matches[1]) + if err != nil { + return nil, fmt.Errorf("sodMax: %w", err) + } + + return &Constraint{ + Key: key, + Type: ConstraintTypeSODMax, + Roles: roles, + MaxCount: maxCount, + }, nil + } + + // Try to match roleMax pattern + if matches := roleMaxPattern.FindStringSubmatch(value); matches != nil { + maxCount, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, fmt.Errorf("invalid max count in roleMax: %w", err) + } + return &Constraint{ + Key: key, + Type: ConstraintTypeRoleMax, + Role: matches[1], + MaxCount: maxCount, + }, nil + } + + // Try to match rolePre pattern + if matches := rolePrePattern.FindStringSubmatch(value); matches != nil { + return &Constraint{ + Key: key, + Type: ConstraintTypeRolePre, + Role: matches[1], + PreReqRole: matches[2], + }, nil + } + + return nil, fmt.Errorf("unrecognized constraint format: %s", value) +} + +// ValidateConstraints validates all constraints against the current policy. +func (model Model) ValidateConstraints() error { + // Check if constraints exist + if len(model["c"]) == 0 { + return nil // No constraints to validate + } + + // Check if RBAC is enabled + if len(model["g"]) == 0 { + return errors.ErrConstraintRequiresRBAC + } + + // Get grouping policy + gAssertion := model["g"]["g"] + if gAssertion == nil { + return errors.ErrConstraintRequiresRBAC + } + + // Validate each constraint + for _, assertion := range model["c"] { + constraint, err := parseConstraint(assertion.Key, assertion.Value) + if err != nil { + return fmt.Errorf("%w: %s", errors.ErrConstraintParsingError, err.Error()) + } + + if err := model.validateConstraint(constraint, gAssertion.Policy); err != nil { + return err + } + } + + return nil +} + +// validateConstraint validates a single constraint against the policy. +func (model Model) validateConstraint(constraint *Constraint, groupingPolicy [][]string) error { + switch constraint.Type { + case ConstraintTypeSOD: + return model.validateSOD(constraint, groupingPolicy) + case ConstraintTypeSODMax: + return model.validateSODMax(constraint, groupingPolicy) + case ConstraintTypeRoleMax: + return model.validateRoleMax(constraint, groupingPolicy) + case ConstraintTypeRolePre: + return model.validateRolePre(constraint, groupingPolicy) + default: + return fmt.Errorf("unknown constraint type") + } +} + +// buildUserRoleMap builds a map of users to their assigned roles from grouping policy. +func buildUserRoleMap(groupingPolicy [][]string) map[string]map[string]bool { + userRoles := make(map[string]map[string]bool) + + for _, rule := range groupingPolicy { + if len(rule) < 2 { + continue + } + user := rule[0] + role := rule[1] + + if userRoles[user] == nil { + userRoles[user] = make(map[string]bool) + } + userRoles[user][role] = true + } + + return userRoles +} + +// validateSOD validates a Separation of Duties constraint. +func (model Model) validateSOD(constraint *Constraint, groupingPolicy [][]string) error { + if len(constraint.Roles) != 2 { + return errors.NewConstraintViolationError(constraint.Key, "sod requires exactly 2 roles") + } + + role1, role2 := constraint.Roles[0], constraint.Roles[1] + userRoles := buildUserRoleMap(groupingPolicy) + + // Check if any user has both roles + for user, roles := range userRoles { + if roles[role1] && roles[role2] { + return errors.NewConstraintViolationError(constraint.Key, + fmt.Sprintf("user '%s' cannot have both roles '%s' and '%s'", user, role1, role2)) + } + } + + return nil +} + +// validateSODMax validates a maximum role count constraint for a role set. +func (model Model) validateSODMax(constraint *Constraint, groupingPolicy [][]string) error { + userRoles := buildUserRoleMap(groupingPolicy) + + // Check if any user has more than maxCount roles from the role set + for user, roles := range userRoles { + count := 0 + for _, role := range constraint.Roles { + if roles[role] { + count++ + } + } + if count > constraint.MaxCount { + return errors.NewConstraintViolationError(constraint.Key, + fmt.Sprintf("user '%s' has %d roles from %v, exceeds maximum of %d", + user, count, constraint.Roles, constraint.MaxCount)) + } + } + + return nil +} + +// validateRoleMax validates a role cardinality constraint. +func (model Model) validateRoleMax(constraint *Constraint, groupingPolicy [][]string) error { + roleCount := 0 + + // Count how many users have this role + for _, rule := range groupingPolicy { + if len(rule) < 2 { + continue + } + role := rule[1] + + if role == constraint.Role { + roleCount++ + } + } + + if roleCount > constraint.MaxCount { + return errors.NewConstraintViolationError(constraint.Key, + fmt.Sprintf("role '%s' assigned to %d users, exceeds maximum of %d", + constraint.Role, roleCount, constraint.MaxCount)) + } + + return nil +} + +// validateRolePre validates a prerequisite role constraint. +func (model Model) validateRolePre(constraint *Constraint, groupingPolicy [][]string) error { + userRoles := buildUserRoleMap(groupingPolicy) + + // Check if any user has the main role without the prerequisite role + for user, roles := range userRoles { + if roles[constraint.Role] && !roles[constraint.PreReqRole] { + return errors.NewConstraintViolationError(constraint.Key, + fmt.Sprintf("user '%s' has role '%s' but lacks prerequisite role '%s'", + user, constraint.Role, constraint.PreReqRole)) + } + } + + return nil +} diff --git a/model/function.go b/model/function.go new file mode 100644 index 0000000..37ca4c0 --- /dev/null +++ b/model/function.go @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package model + +import ( + "sync" + + "github.com/casbin/casbin/v3/util" + "github.com/casbin/govaluate" +) + +// FunctionMap represents the collection of Function. +type FunctionMap struct { + fns *sync.Map +} + +// [string]govaluate.ExpressionFunction + +// AddFunction adds an expression function. +func (fm *FunctionMap) AddFunction(name string, function govaluate.ExpressionFunction) { + fm.fns.LoadOrStore(name, function) +} + +// LoadFunctionMap loads an initial function map. +func LoadFunctionMap() FunctionMap { + fm := &FunctionMap{} + fm.fns = &sync.Map{} + + fm.AddFunction("keyMatch", util.KeyMatchFunc) + fm.AddFunction("keyGet", util.KeyGetFunc) + fm.AddFunction("keyMatch2", util.KeyMatch2Func) + fm.AddFunction("keyGet2", util.KeyGet2Func) + fm.AddFunction("keyMatch3", util.KeyMatch3Func) + fm.AddFunction("keyGet3", util.KeyGet3Func) + fm.AddFunction("keyMatch4", util.KeyMatch4Func) + fm.AddFunction("keyMatch5", util.KeyMatch5Func) + fm.AddFunction("regexMatch", util.RegexMatchFunc) + fm.AddFunction("ipMatch", util.IPMatchFunc) + fm.AddFunction("globMatch", util.GlobMatchFunc) + + return *fm +} + +// GetFunctions return a map with all the functions. +func (fm *FunctionMap) GetFunctions() map[string]govaluate.ExpressionFunction { + ret := make(map[string]govaluate.ExpressionFunction) + + fm.fns.Range(func(k interface{}, v interface{}) bool { + ret[k.(string)] = v.(govaluate.ExpressionFunction) + return true + }) + + return ret +} diff --git a/model/model.go b/model/model.go new file mode 100644 index 0000000..487d35b --- /dev/null +++ b/model/model.go @@ -0,0 +1,442 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package model + +import ( + "container/list" + "errors" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/casbin/casbin/v3/config" + "github.com/casbin/casbin/v3/constant" + "github.com/casbin/casbin/v3/util" +) + +// Model represents the whole access control model. +type Model map[string]AssertionMap + +// AssertionMap is the collection of assertions, can be "r", "p", "g", "e", "m". +type AssertionMap map[string]*Assertion + +const ( + defaultDomain string = "" + defaultSeparator string = "::" +) + +var sectionNameMap = map[string]string{ + "r": "request_definition", + "p": "policy_definition", + "g": "role_definition", + "e": "policy_effect", + "m": "matchers", + "c": "constraint_definition", +} + +// Minimal required sections for a model to be valid. +var requiredSections = []string{"r", "p", "e", "m"} + +func loadAssertion(model Model, cfg config.ConfigInterface, sec string, key string) bool { + value := cfg.String(sectionNameMap[sec] + "::" + key) + return model.AddDef(sec, key, value) +} + +var paramsRegex = regexp.MustCompile(`\((.*?)\)`) + +// getParamsToken Get ParamsToken from Assertion.Value. +func getParamsToken(value string) []string { + paramsString := paramsRegex.FindString(value) + if paramsString == "" { + return nil + } + paramsString = strings.TrimSuffix(strings.TrimPrefix(paramsString, "("), ")") + return strings.Split(paramsString, ",") +} + +// AddDef adds an assertion to the model. +func (model Model) AddDef(sec string, key string, value string) bool { + if value == "" { + return false + } + + ast := Assertion{} + ast.Key = key + ast.Value = value + ast.PolicyMap = make(map[string]int) + ast.FieldIndexMap = make(map[string]int) + + switch sec { + case "r", "p": + ast.Tokens = strings.Split(ast.Value, ",") + for i := range ast.Tokens { + ast.Tokens[i] = key + "_" + strings.TrimSpace(ast.Tokens[i]) + } + case "g": + ast.ParamsTokens = getParamsToken(ast.Value) + ast.Tokens = strings.Split(ast.Value, ",") + ast.Tokens = ast.Tokens[:len(ast.Tokens)-len(ast.ParamsTokens)] + default: + ast.Value = util.RemoveComments(util.EscapeAssertion(ast.Value)) + } + + if sec == "m" { + // Escape backslashes in string literals to match CSV parsing behavior + ast.Value = util.EscapeStringLiterals(ast.Value) + + if strings.Contains(ast.Value, "in") { + ast.Value = strings.ReplaceAll(strings.ReplaceAll(ast.Value, "[", "("), "]", ")") + } + } + + _, ok := model[sec] + if !ok { + model[sec] = make(AssertionMap) + } + + model[sec][key] = &ast + return true +} + +func getKeySuffix(i int) string { + if i == 1 { + return "" + } + + return strconv.Itoa(i) +} + +func loadSection(model Model, cfg config.ConfigInterface, sec string) { + i := 1 + for { + if !loadAssertion(model, cfg, sec, sec+getKeySuffix(i)) { + break + } else { + i++ + } + } +} + +// NewModel creates an empty model. +func NewModel() Model { + m := make(Model) + + return m +} + +// NewModelFromFile creates a model from a .CONF file. +func NewModelFromFile(path string) (Model, error) { + m := NewModel() + + err := m.LoadModel(path) + if err != nil { + return nil, err + } + + return m, nil +} + +// NewModelFromString creates a model from a string which contains model text. +func NewModelFromString(text string) (Model, error) { + m := NewModel() + + err := m.LoadModelFromText(text) + if err != nil { + return nil, err + } + + return m, nil +} + +// LoadModel loads the model from model CONF file. +func (model Model) LoadModel(path string) error { + cfg, err := config.NewConfig(path) + if err != nil { + return err + } + + return model.loadModelFromConfig(cfg) +} + +// LoadModelFromText loads the model from the text. +func (model Model) LoadModelFromText(text string) error { + cfg, err := config.NewConfigFromText(text) + if err != nil { + return err + } + + return model.loadModelFromConfig(cfg) +} + +// loadModelFromConfig loads the model from a config interface. +// It loads all sections defined in sectionNameMap and validates that required sections are present. +// If constraint_definition section exists, it validates all constraints against the current policy. +// Note: Constraint validation is performed during model loading, which may affect loading performance +// and can cause model loading to fail if constraints are violated or invalid. +func (model Model) loadModelFromConfig(cfg config.ConfigInterface) error { + for s := range sectionNameMap { + loadSection(model, cfg, s) + } + ms := make([]string, 0) + for _, rs := range requiredSections { + if !model.hasSection(rs) { + ms = append(ms, sectionNameMap[rs]) + } + } + if len(ms) > 0 { + return fmt.Errorf("missing required sections: %s", strings.Join(ms, ",")) + } + + // Validate constraints after model is loaded + if err := model.ValidateConstraints(); err != nil { + return err + } + + return nil +} + +func (model Model) hasSection(sec string) bool { + section := model[sec] + return section != nil +} + +func (model Model) GetAssertion(sec string, ptype string) (*Assertion, error) { + if model[sec] == nil { + return nil, fmt.Errorf("missing required section %s", sec) + } + if model[sec][ptype] == nil { + return nil, fmt.Errorf("missing required definition %s in section %s", ptype, sec) + } + return model[sec][ptype], nil +} + +// PrintModel prints the model to the log. +func (model Model) PrintModel() { + // Logger has been removed - this is now a no-op +} + +func (model Model) SortPoliciesBySubjectHierarchy() error { + if model["e"]["e"].Value != constant.SubjectPriorityEffect { + return nil + } + g, err := model.GetAssertion("g", "g") + if err != nil { + return err + } + subIndex := 0 + for ptype, assertion := range model["p"] { + domainIndex, err := model.GetFieldIndex(ptype, constant.DomainIndex) + if err != nil { + domainIndex = -1 + } + policies := assertion.Policy + subjectHierarchyMap, err := getSubjectHierarchyMap(g.Policy) + if err != nil { + return err + } + sort.SliceStable(policies, func(i, j int) bool { + domain1, domain2 := defaultDomain, defaultDomain + if domainIndex != -1 { + domain1 = policies[i][domainIndex] + domain2 = policies[j][domainIndex] + } + name1, name2 := getNameWithDomain(domain1, policies[i][subIndex]), getNameWithDomain(domain2, policies[j][subIndex]) + p1 := subjectHierarchyMap[name1] + p2 := subjectHierarchyMap[name2] + return p1 > p2 + }) + for i, policy := range assertion.Policy { + assertion.PolicyMap[strings.Join(policy, ",")] = i + } + } + return nil +} + +func getSubjectHierarchyMap(policies [][]string) (map[string]int, error) { + subjectHierarchyMap := make(map[string]int) + // Tree structure of role + policyMap := make(map[string][]string) + for _, policy := range policies { + if len(policy) < 2 { + return nil, errors.New("policy g expect 2 more params") + } + domain := defaultDomain + if len(policy) != 2 { + domain = policy[2] + } + child := getNameWithDomain(domain, policy[0]) + parent := getNameWithDomain(domain, policy[1]) + policyMap[parent] = append(policyMap[parent], child) + if _, ok := subjectHierarchyMap[child]; !ok { + subjectHierarchyMap[child] = 0 + } + if _, ok := subjectHierarchyMap[parent]; !ok { + subjectHierarchyMap[parent] = 0 + } + subjectHierarchyMap[child] = 1 + } + // Use queues for levelOrder + queue := list.New() + for k, v := range subjectHierarchyMap { + root := k + if v != 0 { + continue + } + lv := 0 + queue.PushBack(root) + for queue.Len() != 0 { + sz := queue.Len() + for i := 0; i < sz; i++ { + node := queue.Front() + queue.Remove(node) + nodeValue := node.Value.(string) + subjectHierarchyMap[nodeValue] = lv + if _, ok := policyMap[nodeValue]; ok { + for _, child := range policyMap[nodeValue] { + queue.PushBack(child) + } + } + } + lv++ + } + } + return subjectHierarchyMap, nil +} + +func getNameWithDomain(domain string, name string) string { + return domain + defaultSeparator + name +} + +func (model Model) SortPoliciesByPriority() error { + for ptype, assertion := range model["p"] { + priorityIndex, err := model.GetFieldIndex(ptype, constant.PriorityIndex) + if err != nil { + continue + } + policies := assertion.Policy + sort.SliceStable(policies, func(i, j int) bool { + p1, err := strconv.Atoi(policies[i][priorityIndex]) + if err != nil { + return true + } + p2, err := strconv.Atoi(policies[j][priorityIndex]) + if err != nil { + return true + } + return p1 < p2 + }) + for i, policy := range assertion.Policy { + assertion.PolicyMap[strings.Join(policy, ",")] = i + } + } + return nil +} + +var ( + pPattern = regexp.MustCompile("^p_") + rPattern = regexp.MustCompile("^r_") +) + +func (model Model) ToText() string { + tokenPatterns := make(map[string]string) + + for _, ptype := range []string{"r", "p"} { + for _, token := range model[ptype][ptype].Tokens { + tokenPatterns[token] = rPattern.ReplaceAllString(pPattern.ReplaceAllString(token, "p."), "r.") + } + } + if strings.Contains(model["e"]["e"].Value, "p_eft") { + tokenPatterns["p_eft"] = "p.eft" + } + s := strings.Builder{} + writeString := func(sec string) { + for ptype := range model[sec] { + value := model[sec][ptype].Value + for tokenPattern, newToken := range tokenPatterns { + value = strings.ReplaceAll(value, tokenPattern, newToken) + } + fmt.Fprintf(&s, "%s = %s\n", sec, value) + } + } + s.WriteString("[request_definition]\n") + writeString("r") + s.WriteString("[policy_definition]\n") + writeString("p") + if _, ok := model["g"]; ok { + s.WriteString("[role_definition]\n") + for ptype := range model["g"] { + fmt.Fprintf(&s, "%s = %s\n", ptype, model["g"][ptype].Value) + } + } + if _, ok := model["c"]; ok { + s.WriteString("[constraint_definition]\n") + for ptype := range model["c"] { + fmt.Fprintf(&s, "%s = %s\n", ptype, model["c"][ptype].Value) + } + } + s.WriteString("[policy_effect]\n") + writeString("e") + s.WriteString("[matchers]\n") + writeString("m") + return s.String() +} + +func (model Model) Copy() Model { + newModel := NewModel() + + for sec, m := range model { + newAstMap := make(AssertionMap) + for ptype, ast := range m { + newAstMap[ptype] = ast.copy() + } + newModel[sec] = newAstMap + } + + return newModel +} + +func (model Model) GetFieldIndex(ptype string, field string) (int, error) { + assertion := model["p"][ptype] + + assertion.FieldIndexMutex.RLock() + if index, ok := assertion.FieldIndexMap[field]; ok { + assertion.FieldIndexMutex.RUnlock() + return index, nil + } + assertion.FieldIndexMutex.RUnlock() + + pattern := fmt.Sprintf("%s_"+field, ptype) + index := -1 + for i, token := range assertion.Tokens { + if token == pattern { + index = i + break + } + } + if index == -1 { + return index, fmt.Errorf(field + " index is not set, please use enforcer.SetFieldIndex() to set index") + } + + assertion.FieldIndexMutex.Lock() + assertion.FieldIndexMap[field] = index + assertion.FieldIndexMutex.Unlock() + + return index, nil +} diff --git a/model/model_test.go b/model/model_test.go new file mode 100644 index 0000000..1b3c8c1 --- /dev/null +++ b/model/model_test.go @@ -0,0 +1,164 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package model + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/casbin/casbin/v3/config" + "github.com/casbin/casbin/v3/constant" +) + +var ( + basicExample = filepath.Join("..", "examples", "basic_model.conf") + basicConfig = &MockConfig{ + data: map[string]string{ + "request_definition::r": "sub, obj, act", + "policy_definition::p": "sub, obj, act", + "policy_effect::e": "some(where (p.eft == allow))", + "matchers::m": "r.sub == p.sub && r.obj == p.obj && r.act == p.act", + }, + } +) + +type MockConfig struct { + data map[string]string + config.ConfigInterface +} + +func (mc *MockConfig) String(key string) string { + return mc.data[key] +} + +func TestNewModel(t *testing.T) { + m := NewModel() + if m == nil { + t.Error("new model should not be nil") + } +} + +func TestNewModelFromFile(t *testing.T) { + m, err := NewModelFromFile(basicExample) + if err != nil { + t.Errorf("model failed to load from file: %s", err) + } + if m == nil { + t.Error("model should not be nil") + } +} + +func TestNewModelFromString(t *testing.T) { + modelBytes, _ := os.ReadFile(basicExample) + modelString := string(modelBytes) + m, err := NewModelFromString(modelString) + if err != nil { + t.Errorf("model failed to load from string: %s", err) + } + if m == nil { + t.Error("model should not be nil") + } +} + +func TestLoadModelFromConfig(t *testing.T) { + m := NewModel() + err := m.loadModelFromConfig(basicConfig) + if err != nil { + t.Error("basic config should not return an error") + } + m = NewModel() + err = m.loadModelFromConfig(&MockConfig{}) + if err == nil { + t.Error("empty config should return error") + } else { + // check for missing sections in message + for _, rs := range requiredSections { + if !strings.Contains(err.Error(), sectionNameMap[rs]) { + t.Errorf("section name: %s should be in message", sectionNameMap[rs]) + } + } + } +} + +func TestHasSection(t *testing.T) { + m := NewModel() + _ = m.loadModelFromConfig(basicConfig) + for _, sec := range requiredSections { + if !m.hasSection(sec) { + t.Errorf("%s section was expected in model", sec) + } + } + m = NewModel() + _ = m.loadModelFromConfig(&MockConfig{}) + for _, sec := range requiredSections { + if m.hasSection(sec) { + t.Errorf("%s section was not expected in model", sec) + } + } +} + +func TestModel_AddDef(t *testing.T) { + m := NewModel() + s := "r" + v := "sub, obj, act" + ok := m.AddDef(s, s, v) + if !ok { + t.Errorf("non empty assertion should be added") + } + ok = m.AddDef(s, s, "") + if ok { + t.Errorf("empty assertion value should not be added") + } +} + +func TestModelToTest(t *testing.T) { + testModelToText(t, "r.sub == p.sub && r.obj == p.obj && r_func(r.act, p.act) && testr_func(r.act, p.act)", "r_sub == p_sub && r_obj == p_obj && r_func(r_act, p_act) && testr_func(r_act, p_act)") + testModelToText(t, "r.sub == p.sub && r.obj == p.obj && p_func(r.act, p.act) && testp_func(r.act, p.act)", "r_sub == p_sub && r_obj == p_obj && p_func(r_act, p_act) && testp_func(r_act, p_act)") +} + +func testModelToText(t *testing.T, mData, mExpected string) { + m := NewModel() + data := map[string]string{ + "r": "sub, obj, act", + "p": "sub, obj, act", + "e": "some(where (p.eft == allow))", + "m": mData, + } + expected := map[string]string{ + "r": "sub, obj, act", + "p": "sub, obj, act", + "e": constant.AllowOverrideEffect, + "m": mExpected, + } + addData := func(ptype string) { + m.AddDef(ptype, ptype, data[ptype]) + } + for ptype := range data { + addData(ptype) + } + newM := NewModel() + print(m.ToText()) + _ = newM.LoadModelFromText(m.ToText()) + for ptype := range data { + if newM[ptype][ptype].Value != expected[ptype] { + t.Errorf("\"%s\" assertion value changed, current value: %s, it should be: %s", ptype, newM[ptype][ptype].Value, expected[ptype]) + } + } +} diff --git a/model/policy.go b/model/policy.go new file mode 100644 index 0000000..7669dfe --- /dev/null +++ b/model/policy.go @@ -0,0 +1,482 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package model + +import ( + "fmt" + "strconv" + "strings" + + "github.com/casbin/casbin/v3/constant" + "github.com/casbin/casbin/v3/rbac" + "github.com/casbin/casbin/v3/util" +) + +type ( + PolicyOp int +) + +const ( + PolicyAdd PolicyOp = iota + PolicyRemove +) + +const DefaultSep = "," + +// BuildIncrementalRoleLinks provides incremental build the role inheritance relations. +func (model Model) BuildIncrementalRoleLinks(rmMap map[string]rbac.RoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error { + if sec == "g" && rmMap[ptype] != nil { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return err + } + return model[sec][ptype].buildIncrementalRoleLinks(rmMap[ptype], op, rules) + } + return nil +} + +// BuildRoleLinks initializes the roles in RBAC. +func (model Model) BuildRoleLinks(rmMap map[string]rbac.RoleManager) error { + model.PrintPolicy() + for ptype, ast := range model["g"] { + if rm := rmMap[ptype]; rm != nil { + err := ast.buildRoleLinks(rm) + if err != nil { + return err + } + } + } + + return nil +} + +// BuildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations. +func (model Model) BuildIncrementalConditionalRoleLinks(condRmMap map[string]rbac.ConditionalRoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error { + if sec == "g" && condRmMap[ptype] != nil { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return err + } + return model[sec][ptype].buildIncrementalConditionalRoleLinks(condRmMap[ptype], op, rules) + } + return nil +} + +// BuildConditionalRoleLinks initializes the roles in RBAC. +func (model Model) BuildConditionalRoleLinks(condRmMap map[string]rbac.ConditionalRoleManager) error { + model.PrintPolicy() + for ptype, ast := range model["g"] { + if condRm := condRmMap[ptype]; condRm != nil { + err := ast.buildConditionalRoleLinks(condRm) + if err != nil { + return err + } + } + } + + return nil +} + +// PrintPolicy prints the policy to log. +func (model Model) PrintPolicy() { + // Logger has been removed - this is now a no-op +} + +// ClearPolicy clears all current policy. +func (model Model) ClearPolicy() { + for _, ast := range model["p"] { + ast.Policy = nil + ast.PolicyMap = map[string]int{} + } + + for _, ast := range model["g"] { + ast.Policy = nil + ast.PolicyMap = map[string]int{} + } +} + +// GetPolicy gets all rules in a policy. +func (model Model) GetPolicy(sec string, ptype string) ([][]string, error) { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return nil, err + } + return model[sec][ptype].Policy, nil +} + +// GetFilteredPolicy gets rules based on field filters from a policy. +func (model Model) GetFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) ([][]string, error) { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return nil, err + } + res := [][]string{} + + for _, rule := range model[sec][ptype].Policy { + matched := true + for i, fieldValue := range fieldValues { + if fieldValue != "" && rule[fieldIndex+i] != fieldValue { + matched = false + break + } + } + + if matched { + res = append(res, rule) + } + } + + return res, nil +} + +// HasPolicyEx determines whether a model has the specified policy rule with error. +func (model Model) HasPolicyEx(sec string, ptype string, rule []string) (bool, error) { + assertion, err := model.GetAssertion(sec, ptype) + if err != nil { + return false, err + } + switch sec { + case "p": + if len(rule) != len(assertion.Tokens) { + return false, fmt.Errorf( + "invalid policy rule size: expected %d, got %d, rule: %v", + len(model["p"][ptype].Tokens), + len(rule), + rule) + } + case "g": + if len(rule) < len(assertion.Tokens) { + return false, fmt.Errorf( + "invalid policy rule size: expected %d, got %d, rule: %v", + len(model["g"][ptype].Tokens), + len(rule), + rule) + } + } + return model.HasPolicy(sec, ptype, rule) +} + +// HasPolicy determines whether a model has the specified policy rule. +func (model Model) HasPolicy(sec string, ptype string, rule []string) (bool, error) { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return false, err + } + _, ok := model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)] + return ok, nil +} + +// HasPolicies determines whether a model has any of the specified policies. If one is found we return true. +func (model Model) HasPolicies(sec string, ptype string, rules [][]string) (bool, error) { + for i := 0; i < len(rules); i++ { + ok, err := model.HasPolicy(sec, ptype, rules[i]) + if err != nil { + return false, err + } + if ok { + return true, nil + } + } + + return false, nil +} + +// AddPolicy adds a policy rule to the model. +func (model Model) AddPolicy(sec string, ptype string, rule []string) error { + assertion, err := model.GetAssertion(sec, ptype) + if err != nil { + return err + } + assertion.Policy = append(assertion.Policy, rule) + assertion.PolicyMap[strings.Join(rule, DefaultSep)] = len(model[sec][ptype].Policy) - 1 + + hasPriority := false + if _, ok := assertion.FieldIndexMap[constant.PriorityIndex]; ok { + hasPriority = true + } + if sec == "p" && hasPriority { + if idxInsert, err := strconv.Atoi(rule[assertion.FieldIndexMap[constant.PriorityIndex]]); err == nil { + i := len(assertion.Policy) - 1 + for ; i > 0; i-- { + idx, err := strconv.Atoi(assertion.Policy[i-1][assertion.FieldIndexMap[constant.PriorityIndex]]) + if err != nil || idx <= idxInsert { + break + } + assertion.Policy[i] = assertion.Policy[i-1] + assertion.PolicyMap[strings.Join(assertion.Policy[i-1], DefaultSep)]++ + } + assertion.Policy[i] = rule + assertion.PolicyMap[strings.Join(rule, DefaultSep)] = i + } + } + return nil +} + +// AddPolicies adds policy rules to the model. +func (model Model) AddPolicies(sec string, ptype string, rules [][]string) error { + _, err := model.AddPoliciesWithAffected(sec, ptype, rules) + return err +} + +// AddPoliciesWithAffected adds policy rules to the model, and returns affected rules. +func (model Model) AddPoliciesWithAffected(sec string, ptype string, rules [][]string) ([][]string, error) { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return nil, err + } + var affected [][]string + for _, rule := range rules { + hashKey := strings.Join(rule, DefaultSep) + _, ok := model[sec][ptype].PolicyMap[hashKey] + if ok { + continue + } + affected = append(affected, rule) + err = model.AddPolicy(sec, ptype, rule) + if err != nil { + return affected, err + } + } + return affected, err +} + +// RemovePolicy removes a policy rule from the model. +func (model Model) RemovePolicy(sec string, ptype string, rule []string) (bool, error) { + ast, err := model.GetAssertion(sec, ptype) + if err != nil { + return false, err + } + key := strings.Join(rule, DefaultSep) + index, ok := ast.PolicyMap[key] + if !ok { + return false, nil + } + + // The order of Policy is significant: it carries the priority semantics of + // the "priority(p_eft)" effector, both for explicit priority models and for + // implicit ones where the priority is the policy order itself. So the rule + // has to be shifted out instead of being swapped with the last one. + ast.Policy = append(ast.Policy[:index], ast.Policy[index+1:]...) + delete(ast.PolicyMap, key) + for i := index; i < len(ast.Policy); i++ { + ast.PolicyMap[strings.Join(ast.Policy[i], DefaultSep)] = i + } + return true, nil +} + +// UpdatePolicy updates a policy rule from the model. +func (model Model) UpdatePolicy(sec string, ptype string, oldRule []string, newRule []string) (bool, error) { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return false, err + } + oldPolicy := strings.Join(oldRule, DefaultSep) + index, ok := model[sec][ptype].PolicyMap[oldPolicy] + if !ok { + return false, nil + } + + model[sec][ptype].Policy[index] = newRule + delete(model[sec][ptype].PolicyMap, oldPolicy) + model[sec][ptype].PolicyMap[strings.Join(newRule, DefaultSep)] = index + + return true, nil +} + +// UpdatePolicies updates a policy rule from the model. +func (model Model) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return false, err + } + rollbackFlag := false + // index -> []{oldIndex, newIndex} + modifiedRuleIndex := make(map[int][]int) + // rollback + defer func() { + if rollbackFlag { + for index, oldNewIndex := range modifiedRuleIndex { + model[sec][ptype].Policy[index] = oldRules[oldNewIndex[0]] + oldPolicy := strings.Join(oldRules[oldNewIndex[0]], DefaultSep) + newPolicy := strings.Join(newRules[oldNewIndex[1]], DefaultSep) + delete(model[sec][ptype].PolicyMap, newPolicy) + model[sec][ptype].PolicyMap[oldPolicy] = index + } + } + }() + + newIndex := 0 + for oldIndex, oldRule := range oldRules { + oldPolicy := strings.Join(oldRule, DefaultSep) + index, ok := model[sec][ptype].PolicyMap[oldPolicy] + if !ok { + rollbackFlag = true + return false, nil + } + + model[sec][ptype].Policy[index] = newRules[newIndex] + delete(model[sec][ptype].PolicyMap, oldPolicy) + model[sec][ptype].PolicyMap[strings.Join(newRules[newIndex], DefaultSep)] = index + modifiedRuleIndex[index] = []int{oldIndex, newIndex} + newIndex++ + } + + return true, nil +} + +// RemovePolicies removes policy rules from the model. +func (model Model) RemovePolicies(sec string, ptype string, rules [][]string) (bool, error) { + affected, err := model.RemovePoliciesWithAffected(sec, ptype, rules) + return len(affected) != 0, err +} + +// RemovePoliciesWithAffected removes policy rules from the model, and returns affected rules. +func (model Model) RemovePoliciesWithAffected(sec string, ptype string, rules [][]string) ([][]string, error) { + assertion, err := model.GetAssertion(sec, ptype) + if err != nil { + return nil, err + } + + var affected [][]string + removeSet := make(map[string]struct{}, len(rules)) + + for _, rule := range rules { + key := strings.Join(rule, DefaultSep) + if _, ok := assertion.PolicyMap[key]; !ok { + continue + } + if _, exists := removeSet[key]; exists { + continue + } + affected = append(affected, rule) + removeSet[key] = struct{}{} + } + + if len(removeSet) == 0 { + return affected, nil + } + + compactPolicy := assertion.Policy[:0] + compactPolicyMap := make(map[string]int, len(assertion.Policy)-len(removeSet)) + for _, policyRule := range assertion.Policy { + key := strings.Join(policyRule, DefaultSep) + if _, ok := removeSet[key]; ok { + continue + } + compactPolicyMap[key] = len(compactPolicy) + compactPolicy = append(compactPolicy, policyRule) + } + + assertion.Policy = compactPolicy + assertion.PolicyMap = compactPolicyMap + + return affected, nil +} + +// RemoveFilteredPolicy removes policy rules based on field filters from the model. +func (model Model) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, [][]string, error) { + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return false, nil, err + } + var tmp [][]string + var effects [][]string + res := false + model[sec][ptype].PolicyMap = map[string]int{} + + for _, rule := range model[sec][ptype].Policy { + matched := true + for i, fieldValue := range fieldValues { + if fieldValue != "" && rule[fieldIndex+i] != fieldValue { + matched = false + break + } + } + + if matched { + effects = append(effects, rule) + } else { + tmp = append(tmp, rule) + model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)] = len(tmp) - 1 + } + } + + if len(tmp) != len(model[sec][ptype].Policy) { + model[sec][ptype].Policy = tmp + res = true + } + + return res, effects, nil +} + +// GetValuesForFieldInPolicy gets all values for a field for all rules in a policy, duplicated values are removed. +func (model Model) GetValuesForFieldInPolicy(sec string, ptype string, fieldIndex int) ([]string, error) { + values := []string{} + + _, err := model.GetAssertion(sec, ptype) + if err != nil { + return nil, err + } + + for _, rule := range model[sec][ptype].Policy { + values = append(values, rule[fieldIndex]) + } + + util.ArrayRemoveDuplicates(&values) + + return values, nil +} + +// GetValuesForFieldInPolicyAllTypes gets all values for a field for all rules in a policy of all ptypes, duplicated values are removed. +func (model Model) GetValuesForFieldInPolicyAllTypes(sec string, fieldIndex int) ([]string, error) { + values := []string{} + + for ptype := range model[sec] { + v, err := model.GetValuesForFieldInPolicy(sec, ptype, fieldIndex) + if err != nil { + return nil, err + } + values = append(values, v...) + } + + util.ArrayRemoveDuplicates(&values) + + return values, nil +} + +// GetValuesForFieldInPolicyAllTypesByName gets all values for a field for all rules in a policy of all ptypes, duplicated values are removed. +func (model Model) GetValuesForFieldInPolicyAllTypesByName(sec string, field string) ([]string, error) { + values := []string{} + + for ptype := range model[sec] { + // GetFieldIndex will return (-1, err) if field is not found, ignore it + index, err := model.GetFieldIndex(ptype, field) + if err != nil { + continue + } + v, err := model.GetValuesForFieldInPolicy(sec, ptype, index) + if err != nil { + return nil, err + } + values = append(values, v...) + } + + util.ArrayRemoveDuplicates(&values) + + return values, nil +} diff --git a/model_b_test.go b/model_b_test.go new file mode 100644 index 0000000..18ae650 --- /dev/null +++ b/model_b_test.go @@ -0,0 +1,294 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + "testing" + + "github.com/casbin/casbin/v3/util" +) + +func rawEnforce(sub string, obj string, act string) bool { + policy := [2][3]string{{"alice", "data1", "read"}, {"bob", "data2", "write"}} + for _, rule := range policy { + if sub == rule[0] && obj == rule[1] && act == rule[2] { + return true + } + } + return false +} + +func BenchmarkRaw(b *testing.B) { + for i := 0; i < b.N; i++ { + rawEnforce("alice", "data1", "read") + } +} + +func BenchmarkBasicModel(b *testing.B) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkRBACModel(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data2", "read") + } +} + +func BenchmarkRBACModelSizes(b *testing.B) { + cases := []struct { + name string + roles int + resources int + users int + }{ + {name: "small", roles: 100, resources: 10, users: 1000}, + {name: "medium", roles: 1000, resources: 100, users: 10000}, + {name: "large", roles: 10000, resources: 1000, users: 100000}, + } + for _, c := range cases { + c := c + + e, err := NewEnforcer("examples/rbac_model.conf") + if err != nil { + b.Fatal(err) + } + + pPolicies := make([][]string, c.roles) + for i := range pPolicies { + pPolicies[i] = []string{ + fmt.Sprintf("group-has-a-very-long-name-%d", i), + fmt.Sprintf("data-has-a-very-long-name-%d", i%c.resources), + "read", + } + } + if _, err := e.AddPolicies(pPolicies); err != nil { + b.Fatal(err) + } + + gPolicies := make([][]string, c.users) + for i := range gPolicies { + gPolicies[i] = []string{ + fmt.Sprintf("user-has-a-very-long-name-%d", i), + fmt.Sprintf("group-has-a-very-long-name-%d", i%c.roles), + } + } + if _, err := e.AddGroupingPolicies(gPolicies); err != nil { + b.Fatal(err) + } + + // Set up enforcements, alternating between things a user can access + // and things they cannot. Use 17 tests so that we get a variety of users + // and roles rather than always landing on a multiple of 2/10/whatever. + enforcements := make([][]interface{}, 17) + for i := range enforcements { + userNum := (c.users / len(enforcements)) * i + roleNum := userNum % c.roles + resourceNum := roleNum % c.resources + if i%2 == 0 { + resourceNum += 1 + resourceNum %= c.resources + } + enforcements[i] = []interface{}{ + fmt.Sprintf("user-has-a-very-long-name-%d", userNum), + fmt.Sprintf("data-has-a-very-long-name-%d", resourceNum), + "read", + } + } + + b.Run(c.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = e.Enforce(enforcements[i%len(enforcements)]...) + } + }) + } +} + +func BenchmarkRBACModelSmall(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_model.conf") + + // 100 roles, 10 resources. + for i := 0; i < 100; i++ { + _, err := e.AddPolicy(fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read") + if err != nil { + b.Fatal(err) + } + } + + // 1000 users. + for i := 0; i < 1000; i++ { + _, err := e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)) + if err != nil { + b.Fatal(err) + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("user501", "data9", "read") + } +} + +func BenchmarkRBACModelMedium(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_model.conf") + + // 1000 roles, 100 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 1000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 10000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("user5001", "data99", "read") + } +} + +func BenchmarkRBACModelLarge(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 100000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 100000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("user50001", "data999", "read") + } +} + +func BenchmarkRBACModelWithResourceRoles(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_with_resource_roles_model.conf", "examples/rbac_with_resource_roles_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkRBACModelWithDomains(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "domain1", "data1", "read") + } +} + +func BenchmarkABACModel(b *testing.B) { + e, _ := NewEnforcer("examples/abac_model.conf") + data1 := newTestResource("data1", "alice") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", data1, "read") + } +} + +func BenchmarkABACRuleModel(b *testing.B) { + e, _ := NewEnforcer("examples/abac_rule_model.conf") + sub := newTestSubject("alice", 18) + + for i := 0; i < 1000; i++ { + _, _ = e.AddPolicy("r.sub.Age > 20", fmt.Sprintf("data%d", i), "read") + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce(sub, "data100", "read") + } +} + +func BenchmarkKeyMatchModel(b *testing.B) { + e, _ := NewEnforcer("examples/keymatch_model.conf", "examples/keymatch_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "/alice_data/resource1", "GET") + } +} + +func BenchmarkRBACModelWithDeny(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_with_deny_model.conf", "examples/rbac_with_deny_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkPriorityModel(b *testing.B) { + e, _ := NewEnforcer("examples/priority_model.conf", "examples/priority_policy.csv") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkRBACModelWithDomainPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedDomainMatchingFunc("g", "", util.KeyMatch4) + _ = e.BuildRoleLinks() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("staffUser1001", "/orgs/1/sites/site001", "App001.Module001.Action1001") + } +} diff --git a/model_test.go b/model_test.go new file mode 100644 index 0000000..5959fd5 --- /dev/null +++ b/model_test.go @@ -0,0 +1,834 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" + + "github.com/casbin/casbin/v3/model" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" + "github.com/casbin/casbin/v3/rbac" + "github.com/casbin/casbin/v3/util" +) + +func testEnforce(t *testing.T, e *Enforcer, sub interface{}, obj interface{}, act string, res bool) { + t.Helper() + if myRes, err := e.Enforce(sub, obj, act); err != nil { + t.Errorf("Enforce Error: %s", err) + } else if myRes != res { + t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res) + } +} + +func testEnforceWithoutUsers(t *testing.T, e *Enforcer, obj string, act string, res bool) { + t.Helper() + if myRes, _ := e.Enforce(obj, act); myRes != res { + t.Errorf("%s, %s: %t, supposed to be %t", obj, act, myRes, res) + } +} + +func testDomainEnforce(t *testing.T, e *Enforcer, sub string, dom string, obj string, act string, res bool) { + t.Helper() + if myRes, err := e.Enforce(sub, dom, obj, act); err != nil { + t.Errorf("Enforce Error: %s", err) + } else if myRes != res { + t.Errorf("%s, %s, %s, %s: %t, supposed to be %t", sub, dom, obj, act, myRes, res) + } +} + +func TestBasicModel(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestBasicModelWithoutSpaces(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model_without_spaces.conf", "examples/basic_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestBasicModelNoPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model.conf") + + testEnforce(t, e, "alice", "data1", "read", false) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", false) +} + +func TestBasicModelWithRoot(t *testing.T) { + e, _ := NewEnforcer("examples/basic_with_root_model.conf", "examples/basic_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) + testEnforce(t, e, "root", "data1", "read", true) + testEnforce(t, e, "root", "data1", "write", true) + testEnforce(t, e, "root", "data2", "read", true) + testEnforce(t, e, "root", "data2", "write", true) +} + +func TestBasicModelWithRootNoPolicy(t *testing.T) { + e, _ := NewEnforcer("examples/basic_with_root_model.conf") + + testEnforce(t, e, "alice", "data1", "read", false) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", false) + testEnforce(t, e, "root", "data1", "read", true) + testEnforce(t, e, "root", "data1", "write", true) + testEnforce(t, e, "root", "data2", "read", true) + testEnforce(t, e, "root", "data2", "write", true) +} + +func TestBasicModelWithoutUsers(t *testing.T) { + e, _ := NewEnforcer("examples/basic_without_users_model.conf", "examples/basic_without_users_policy.csv") + + testEnforceWithoutUsers(t, e, "data1", "read", true) + testEnforceWithoutUsers(t, e, "data1", "write", false) + testEnforceWithoutUsers(t, e, "data2", "read", false) + testEnforceWithoutUsers(t, e, "data2", "write", true) +} + +func TestBasicModelWithoutResources(t *testing.T) { + e, _ := NewEnforcer("examples/basic_without_resources_model.conf", "examples/basic_without_resources_policy.csv") + + testEnforceWithoutUsers(t, e, "alice", "read", true) + testEnforceWithoutUsers(t, e, "alice", "write", false) + testEnforceWithoutUsers(t, e, "bob", "read", false) + testEnforceWithoutUsers(t, e, "bob", "write", true) +} + +func TestRBACModel(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestRBACModelWithResourceRoles(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_resource_roles_model.conf", "examples/rbac_with_resource_roles_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", true) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestRBACModelWithDomains(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true) + testDomainEnforce(t, e, "alice", "domain1", "data2", "read", false) + testDomainEnforce(t, e, "alice", "domain1", "data2", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "read", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data2", "read", true) + testDomainEnforce(t, e, "bob", "domain2", "data2", "write", true) +} + +func TestRBACModelWithDomainsAtRuntime(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf") + + _, _ = e.AddPolicy("admin", "domain1", "data1", "read") + _, _ = e.AddPolicy("admin", "domain1", "data1", "write") + _, _ = e.AddPolicy("admin", "domain2", "data2", "read") + _, _ = e.AddPolicy("admin", "domain2", "data2", "write") + + _, _ = e.AddGroupingPolicy("alice", "admin", "domain1") + _, _ = e.AddGroupingPolicy("bob", "admin", "domain2") + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true) + testDomainEnforce(t, e, "alice", "domain1", "data2", "read", false) + testDomainEnforce(t, e, "alice", "domain1", "data2", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "read", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data2", "read", true) + testDomainEnforce(t, e, "bob", "domain2", "data2", "write", true) + + // Remove all policy rules related to domain1 and data1. + _, _ = e.RemoveFilteredPolicy(1, "domain1", "data1") + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", false) + testDomainEnforce(t, e, "alice", "domain1", "data1", "write", false) + testDomainEnforce(t, e, "alice", "domain1", "data2", "read", false) + testDomainEnforce(t, e, "alice", "domain1", "data2", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "read", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data2", "read", true) + testDomainEnforce(t, e, "bob", "domain2", "data2", "write", true) + + // Remove the specified policy rule. + _, _ = e.RemovePolicy("admin", "domain2", "data2", "read") + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", false) + testDomainEnforce(t, e, "alice", "domain1", "data1", "write", false) + testDomainEnforce(t, e, "alice", "domain1", "data2", "read", false) + testDomainEnforce(t, e, "alice", "domain1", "data2", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "read", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data2", "read", false) + testDomainEnforce(t, e, "bob", "domain2", "data2", "write", true) +} + +func TestRBACModelWithDomainsAtRuntimeMockAdapter(t *testing.T) { + adapter := fileadapter.NewAdapterMock("examples/rbac_with_domains_policy.csv") + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", adapter) + + _, _ = e.AddPolicy("admin", "domain3", "data1", "read") + _, _ = e.AddGroupingPolicy("alice", "admin", "domain3") + + testDomainEnforce(t, e, "alice", "domain3", "data1", "read", true) + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true) + _, _ = e.RemoveFilteredPolicy(1, "domain1", "data1") + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", false) + + testDomainEnforce(t, e, "bob", "domain2", "data2", "read", true) + _, _ = e.RemovePolicy("admin", "domain2", "data2", "read") + testDomainEnforce(t, e, "bob", "domain2", "data2", "read", false) +} + +func TestRBACModelWithDomainTokenRename(t *testing.T) { + // Test that renaming the domain token from "dom" to another name (e.g., "dom1") + // still works correctly. This is a regression test for the issue where the + // hardcoded "r_dom" and "p_dom" strings prevented proper domain matching. + + // Test with standard "dom" token + modelText1 := ` +[request_definition] +r = sub, dom, obj, act + +[policy_definition] +p = sub, dom, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom) && keyMatch(r.dom, p.dom) && r.obj == p.obj && r.act == p.act +` + m1, _ := model.NewModelFromString(modelText1) + e1, _ := NewEnforcer(m1) + _, _ = e1.AddPolicy("admin", "domain1", "data1", "read") + _, _ = e1.AddGroupingPolicy("alice", "admin", "domain*") + + testDomainEnforce(t, e1, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e1, "alice", "domain2", "data1", "read", false) + + // Test with renamed "dom1" token + modelText2 := ` +[request_definition] +r = sub, dom1, obj, act + +[policy_definition] +p = sub, dom1, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.dom1) && keyMatch(r.dom1, p.dom1) && r.obj == p.obj && r.act == p.act +` + m2, _ := model.NewModelFromString(modelText2) + e2, _ := NewEnforcer(m2) + _, _ = e2.AddPolicy("admin", "domain1", "data1", "read") + _, _ = e2.AddGroupingPolicy("alice", "admin", "domain*") + + testDomainEnforce(t, e2, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e2, "alice", "domain2", "data1", "read", false) + + // Test with renamed "tenant" token + modelText3 := ` +[request_definition] +r = sub, tenant, obj, act + +[policy_definition] +p = sub, tenant, obj, act + +[role_definition] +g = _, _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub, r.tenant) && keyMatch(r.tenant, p.tenant) && r.obj == p.obj && r.act == p.act +` + m3, _ := model.NewModelFromString(modelText3) + e3, _ := NewEnforcer(m3) + _, _ = e3.AddPolicy("admin", "domain1", "data1", "read") + _, _ = e3.AddGroupingPolicy("alice", "admin", "domain*") + + testDomainEnforce(t, e3, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e3, "alice", "domain2", "data1", "read", false) +} + +func TestRBACModelWithDeny(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_deny_model.conf", "examples/rbac_with_deny_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestRBACModelWithOnlyDeny(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_not_deny_model.conf", "examples/rbac_with_deny_policy.csv") + + testEnforce(t, e, "alice", "data2", "write", false) +} + +func TestRBACModelWithCustomData(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + // You can add custom data to a grouping policy, Casbin will ignore it. It is only meaningful to the caller. + // This feature can be used to store information like whether "bob" is an end user (so no subject will inherit "bob") + // For Casbin, it is equivalent to: e.AddGroupingPolicy("bob", "data2_admin") + _, _ = e.AddGroupingPolicy("bob", "data2_admin", "custom_data") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", true) + testEnforce(t, e, "bob", "data2", "write", true) + + // You should also take the custom data as a parameter when deleting a grouping policy. + // e.RemoveGroupingPolicy("bob", "data2_admin") won't work. + // Or you can remove it by using RemoveFilteredGroupingPolicy(). + _, _ = e.RemoveGroupingPolicy("bob", "data2_admin", "custom_data") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestRBACModelWithPattern(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv") + + // Here's a little confusing: the matching function here is not the custom function used in matcher. + // It is the matching function used by "g" (and "g2", "g3" if any..) + // You can see in policy that: "g2, /book/:id, book_group", so in "g2()" function in the matcher, instead + // of checking whether "/book/:id" equals the obj: "/book/1", it checks whether the pattern matches. + // You can see it as normal RBAC: "/book/:id" == "/book/1" becomes KeyMatch2("/book/:id", "/book/1") + e.AddNamedMatchingFunc("g2", "KeyMatch2", util.KeyMatch2) + e.AddNamedMatchingFunc("g", "KeyMatch2", util.KeyMatch2) + testEnforce(t, e, "any_user", "/pen3/1", "GET", true) + testEnforce(t, e, "/book/user/1", "/pen4/1", "GET", true) + + testEnforce(t, e, "/book/user/1", "/pen4/1", "POST", true) + + testEnforce(t, e, "alice", "/book/1", "GET", true) + testEnforce(t, e, "alice", "/book/2", "GET", true) + testEnforce(t, e, "alice", "/pen/1", "GET", true) + testEnforce(t, e, "alice", "/pen/2", "GET", false) + testEnforce(t, e, "bob", "/book/1", "GET", false) + testEnforce(t, e, "bob", "/book/2", "GET", false) + testEnforce(t, e, "bob", "/pen/1", "GET", true) + testEnforce(t, e, "bob", "/pen/2", "GET", true) + + // AddMatchingFunc() is actually setting a function because only one function is allowed, + // so when we set "KeyMatch3", we are actually replacing "KeyMatch2" with "KeyMatch3". + e.AddNamedMatchingFunc("g2", "KeyMatch2", util.KeyMatch3) + testEnforce(t, e, "alice", "/book2/1", "GET", true) + testEnforce(t, e, "alice", "/book2/2", "GET", true) + testEnforce(t, e, "alice", "/pen2/1", "GET", true) + testEnforce(t, e, "alice", "/pen2/2", "GET", false) + testEnforce(t, e, "bob", "/book2/1", "GET", false) + testEnforce(t, e, "bob", "/book2/2", "GET", false) + testEnforce(t, e, "bob", "/pen2/1", "GET", true) + testEnforce(t, e, "bob", "/pen2/2", "GET", true) +} + +func TestRBACModelWithDifferentTypesOfRoles(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_different_types_of_roles_model.conf", "examples/rbac_with_different_types_of_roles_policy.csv") + + g, err := e.GetNamedGroupingPolicy("g") + if err != nil { + t.Error(err) + } + + for _, gp := range g { + if len(gp) != 5 { + t.Error("g parameters' num isn't 5") + return + } + e.AddNamedDomainLinkConditionFunc("g", gp[0], gp[1], gp[2], util.TimeMatchFunc) + } + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", true) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", true) + testEnforce(t, e, "bob", "data2", "write", false) + testEnforce(t, e, "carol", "data1", "read", false) + testEnforce(t, e, "carol", "data1", "write", false) + testEnforce(t, e, "carol", "data2", "read", false) + testEnforce(t, e, "carol", "data2", "write", false) +} + +type testCustomRoleManager struct{} + +func NewRoleManager() rbac.RoleManager { + return &testCustomRoleManager{} +} +func (rm *testCustomRoleManager) Clear() error { return nil } +func (rm *testCustomRoleManager) AddLink(name1 string, name2 string, domain ...string) error { + return nil +} + +func (rm *testCustomRoleManager) BuildRelationship(name1 string, name2 string, domain ...string) error { + return nil +} + +func (rm *testCustomRoleManager) DeleteLink(name1 string, name2 string, domain ...string) error { + return nil +} + +func (rm *testCustomRoleManager) HasLink(name1 string, name2 string, domain ...string) (bool, error) { + if name1 == "alice" && name2 == "alice" { + return true, nil + } else if name1 == "alice" && name2 == "data2_admin" { + return true, nil + } else if name1 == "bob" && name2 == "bob" { + return true, nil + } + return false, nil +} + +func (rm *testCustomRoleManager) GetRoles(name string, domain ...string) ([]string, error) { + return []string{}, nil +} + +func (rm *testCustomRoleManager) GetUsers(name string, domain ...string) ([]string, error) { + return []string{}, nil +} + +func (rm *testCustomRoleManager) GetDomains(name string) ([]string, error) { + return []string{}, nil +} + +func (rm *testCustomRoleManager) GetAllDomains() ([]string, error) { + return []string{}, nil +} +func (rm *testCustomRoleManager) PrintRoles() error { return nil } + +func (rm *testCustomRoleManager) Match(str string, pattern string) bool { return true } +func (rm *testCustomRoleManager) AddMatchingFunc(name string, fn rbac.MatchingFunc) {} +func (rm *testCustomRoleManager) AddDomainMatchingFunc(name string, fn rbac.MatchingFunc) {} + +func (rm *testCustomRoleManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) { +} + +func (rm *testCustomRoleManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) { +} + +func (rm *testCustomRoleManager) AddDomainLinkConditionFunc(user string, role string, domain string, fn rbac.LinkConditionFunc) { +} + +func (rm *testCustomRoleManager) SetDomainLinkConditionFuncParams(user string, role string, domain string, params ...string) { +} + +func (rm *testCustomRoleManager) DeleteDomain(domain string) error { + return nil +} + +func (rm *testCustomRoleManager) GetImplicitRoles(name string, domain ...string) ([]string, error) { + return []string{}, nil +} + +func (rm *testCustomRoleManager) GetImplicitUsers(name string, domain ...string) ([]string, error) { + return []string{}, nil +} + +func TestRBACModelWithCustomRoleManager(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + e.SetRoleManager(NewRoleManager()) + _ = e.LoadModel() + _ = e.LoadPolicy() + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestKeyMatchModel(t *testing.T) { + e, _ := NewEnforcer("examples/keymatch_model.conf", "examples/keymatch_policy.csv") + + testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true) + testEnforce(t, e, "alice", "/alice_data/resource1", "POST", true) + testEnforce(t, e, "alice", "/alice_data/resource2", "GET", true) + testEnforce(t, e, "alice", "/alice_data/resource2", "POST", false) + testEnforce(t, e, "alice", "/bob_data/resource1", "GET", false) + testEnforce(t, e, "alice", "/bob_data/resource1", "POST", false) + testEnforce(t, e, "alice", "/bob_data/resource2", "GET", false) + testEnforce(t, e, "alice", "/bob_data/resource2", "POST", false) + + testEnforce(t, e, "bob", "/alice_data/resource1", "GET", false) + testEnforce(t, e, "bob", "/alice_data/resource1", "POST", false) + testEnforce(t, e, "bob", "/alice_data/resource2", "GET", true) + testEnforce(t, e, "bob", "/alice_data/resource2", "POST", false) + testEnforce(t, e, "bob", "/bob_data/resource1", "GET", false) + testEnforce(t, e, "bob", "/bob_data/resource1", "POST", true) + testEnforce(t, e, "bob", "/bob_data/resource2", "GET", false) + testEnforce(t, e, "bob", "/bob_data/resource2", "POST", true) + + testEnforce(t, e, "cathy", "/cathy_data", "GET", true) + testEnforce(t, e, "cathy", "/cathy_data", "POST", true) + testEnforce(t, e, "cathy", "/cathy_data", "DELETE", false) +} + +func TestKeyMatch2Model(t *testing.T) { + e, _ := NewEnforcer("examples/keymatch2_model.conf", "examples/keymatch2_policy.csv") + + testEnforce(t, e, "alice", "/alice_data", "GET", false) + testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true) + testEnforce(t, e, "alice", "/alice_data2/myid", "GET", false) + testEnforce(t, e, "alice", "/alice_data2/myid/using/res_id", "GET", true) +} + +func CustomFunction(key1 string, key2 string) bool { + if key1 == "/alice_data2/myid/using/res_id" && key2 == "/alice_data/:resource" { + return true + } else if key1 == "/alice_data2/myid/using/res_id" && key2 == "/alice_data2/:id/using/:resId" { + return true + } else { + return false + } +} + +func CustomFunctionWrapper(args ...interface{}) (interface{}, error) { + key1 := args[0].(string) + key2 := args[1].(string) + + return CustomFunction(key1, key2), nil +} + +func TestKeyMatchCustomModel(t *testing.T) { + e, _ := NewEnforcer("examples/keymatch_custom_model.conf", "examples/keymatch2_policy.csv") + + e.AddFunction("keyMatchCustom", CustomFunctionWrapper) + + testEnforce(t, e, "alice", "/alice_data2/myid", "GET", false) + testEnforce(t, e, "alice", "/alice_data2/myid/using/res_id", "GET", true) +} + +func TestIPMatchModel(t *testing.T) { + e, _ := NewEnforcer("examples/ipmatch_model.conf", "examples/ipmatch_policy.csv") + + testEnforce(t, e, "192.168.2.123", "data1", "read", true) + testEnforce(t, e, "192.168.2.123", "data1", "write", false) + testEnforce(t, e, "192.168.2.123", "data2", "read", false) + testEnforce(t, e, "192.168.2.123", "data2", "write", false) + + testEnforce(t, e, "192.168.0.123", "data1", "read", false) + testEnforce(t, e, "192.168.0.123", "data1", "write", false) + testEnforce(t, e, "192.168.0.123", "data2", "read", false) + testEnforce(t, e, "192.168.0.123", "data2", "write", false) + + testEnforce(t, e, "10.0.0.5", "data1", "read", false) + testEnforce(t, e, "10.0.0.5", "data1", "write", false) + testEnforce(t, e, "10.0.0.5", "data2", "read", false) + testEnforce(t, e, "10.0.0.5", "data2", "write", true) + + testEnforce(t, e, "192.168.0.1", "data1", "read", false) + testEnforce(t, e, "192.168.0.1", "data1", "write", false) + testEnforce(t, e, "192.168.0.1", "data2", "read", false) + testEnforce(t, e, "192.168.0.1", "data2", "write", false) +} + +func TestGlobMatchModel(t *testing.T) { + e, _ := NewEnforcer("examples/glob_model.conf", "examples/glob_policy.csv") + testEnforce(t, e, "u1", "/foo/", "read", true) + testEnforce(t, e, "u1", "/foo", "read", false) + testEnforce(t, e, "u1", "/foo/subprefix", "read", true) + testEnforce(t, e, "u1", "foo", "read", false) + + testEnforce(t, e, "u2", "/foosubprefix", "read", true) + testEnforce(t, e, "u2", "/foo/subprefix", "read", false) + testEnforce(t, e, "u2", "foo", "read", false) + + testEnforce(t, e, "u3", "/prefix/foo/subprefix", "read", true) + testEnforce(t, e, "u3", "/prefix/foo/", "read", true) + testEnforce(t, e, "u3", "/prefix/foo", "read", false) + + testEnforce(t, e, "u4", "/foo", "read", false) + testEnforce(t, e, "u4", "foo", "read", true) +} + +func TestPriorityModel(t *testing.T) { + e, _ := NewEnforcer("examples/priority_model.conf", "examples/priority_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", true) + testEnforce(t, e, "bob", "data2", "write", false) +} + +func TestPriorityExplicitRemovePolicyKeepsOrder(t *testing.T) { + e, _ := NewEnforcer("examples/priority_model_explicit.conf") + + if _, err := e.AddPolicies([][]string{ + {"1", "carol", "data9", "read", "allow"}, + {"2", "alice", "data1", "read", "deny"}, + {"3", "alice", "data1", "read", "allow"}, + }); err != nil { + t.Fatalf("AddPolicies: %v", err) + } + testEnforce(t, e, "alice", "data1", "read", false) + + // Removing an unrelated rule must not reorder the remaining ones, otherwise + // the priority 3 rule would take precedence over the priority 2 one. + if _, err := e.RemovePolicy("1", "carol", "data9", "read", "allow"); err != nil { + t.Fatalf("RemovePolicy: %v", err) + } + testEnforce(t, e, "alice", "data1", "read", false) +} + +func TestPriorityModelIndeterminate(t *testing.T) { + e, _ := NewEnforcer("examples/priority_model.conf", "examples/priority_indeterminate_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", false) +} + +func TestRBACModelInMultiLines(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model_in_multi_line.conf", "examples/rbac_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestCommentModel(t *testing.T) { + e, _ := NewEnforcer("examples/comment_model.conf", "examples/basic_policy.csv") + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestDomainMatchModel(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domain_pattern_model.conf", "examples/rbac_with_domain_pattern_policy.csv") + e.AddNamedDomainMatchingFunc("g", "keyMatch2", util.KeyMatch2) + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true) + testDomainEnforce(t, e, "alice", "domain1", "data2", "read", false) + testDomainEnforce(t, e, "alice", "domain1", "data2", "write", false) + testDomainEnforce(t, e, "alice", "domain2", "data2", "read", true) + testDomainEnforce(t, e, "alice", "domain2", "data2", "write", true) + testDomainEnforce(t, e, "bob", "domain2", "data1", "read", false) + testDomainEnforce(t, e, "bob", "domain2", "data1", "write", false) + testDomainEnforce(t, e, "bob", "domain2", "data2", "read", true) + testDomainEnforce(t, e, "bob", "domain2", "data2", "write", true) +} + +func TestAllMatchModel(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_all_pattern_model.conf", "examples/rbac_with_all_pattern_policy.csv") + e.AddNamedMatchingFunc("g", "keyMatch2", util.KeyMatch2) + e.AddNamedDomainMatchingFunc("g", "keyMatch2", util.KeyMatch2) + + testDomainEnforce(t, e, "alice", "domain1", "/book/1", "read", true) + testDomainEnforce(t, e, "alice", "domain1", "/book/1", "write", false) + testDomainEnforce(t, e, "alice", "domain2", "/book/1", "read", false) + testDomainEnforce(t, e, "alice", "domain2", "/book/1", "write", true) +} + +func TestTemporalRolesModel(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_temporal_roles_model.conf", "examples/rbac_with_temporal_roles_policy.csv") + + e.AddNamedLinkConditionFunc("g", "alice", "data2_admin", util.TimeMatchFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data3_admin", util.TimeMatchFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data4_admin", util.TimeMatchFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data5_admin", util.TimeMatchFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data6_admin", util.TimeMatchFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data7_admin", util.TimeMatchFunc) + e.AddNamedLinkConditionFunc("g", "alice", "data8_admin", util.TimeMatchFunc) + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", true) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "alice", "data3", "read", true) + testEnforce(t, e, "alice", "data3", "write", true) + testEnforce(t, e, "alice", "data4", "read", true) + testEnforce(t, e, "alice", "data4", "write", true) + testEnforce(t, e, "alice", "data5", "read", true) + testEnforce(t, e, "alice", "data5", "write", true) + testEnforce(t, e, "alice", "data6", "read", false) + testEnforce(t, e, "alice", "data6", "write", false) + testEnforce(t, e, "alice", "data7", "read", true) + testEnforce(t, e, "alice", "data7", "write", true) + testEnforce(t, e, "alice", "data8", "read", false) + testEnforce(t, e, "alice", "data8", "write", false) +} + +func TestTemporalRolesModelWithDomain(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domain_temporal_roles_model.conf", "examples/rbac_with_domain_temporal_roles_policy.csv") + + e.AddNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data3_admin", "domain3", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data4_admin", "domain4", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data5_admin", "domain5", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data6_admin", "domain6", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data7_admin", "domain7", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "data8_admin", "domain8", util.TimeMatchFunc) + + testDomainEnforce(t, e, "alice", "domain1", "data1", "read", true) + testDomainEnforce(t, e, "alice", "domain1", "data1", "write", true) + testDomainEnforce(t, e, "alice", "domain2", "data2", "read", false) + testDomainEnforce(t, e, "alice", "domain2", "data2", "write", false) + testDomainEnforce(t, e, "alice", "domain3", "data3", "read", true) + testDomainEnforce(t, e, "alice", "domain3", "data3", "write", true) + testDomainEnforce(t, e, "alice", "domain4", "data4", "read", true) + testDomainEnforce(t, e, "alice", "domain4", "data4", "write", true) + testDomainEnforce(t, e, "alice", "domain5", "data5", "read", true) + testDomainEnforce(t, e, "alice", "domain5", "data5", "write", true) + testDomainEnforce(t, e, "alice", "domain6", "data6", "read", false) + testDomainEnforce(t, e, "alice", "domain6", "data6", "write", false) + testDomainEnforce(t, e, "alice", "domain7", "data7", "read", true) + testDomainEnforce(t, e, "alice", "domain7", "data7", "write", true) + testDomainEnforce(t, e, "alice", "domain8", "data8", "read", false) + testDomainEnforce(t, e, "alice", "domain8", "data8", "write", false) + + testDomainEnforce(t, e, "alice", "domain_not_exist", "data1", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data1", "write", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data2", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data2", "write", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data3", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data3", "write", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data4", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data4", "write", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data5", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data5", "write", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data6", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data6", "write", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data7", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data7", "write", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data8", "read", false) + testDomainEnforce(t, e, "alice", "domain_not_exist", "data8", "write", false) +} + +func TestTemporalRolesModelWithDomainTransitive(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domain_temporal_roles_model.conf", "examples/rbac_with_domain_temporal_roles_transitive_policy.csv") + + e.AddNamedDomainLinkConditionFunc("g", "alice", "role_mid", "domainx", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "role_mid", "role_top", "domainx", util.TimeMatchFunc) + e.AddNamedDomainLinkConditionFunc("g", "alice", "role_solo", "domainx", util.TimeMatchFunc) + + // single-hop expired link -> correctly denied (control) + testDomainEnforce(t, e, "alice", "domainx", "data_solo", "read", false) + // two-hop chain whose 2nd link is expired -> must be denied as well + testDomainEnforce(t, e, "alice", "domainx", "data_top", "read", false) +} + +func TestReBACModel(t *testing.T) { + e, _ := NewEnforcer("examples/rebac_model.conf", "examples/rebac_policy.csv") + + testEnforce(t, e, "alice", "doc1", "read", true) + testEnforce(t, e, "alice", "doc1", "write", false) + testEnforce(t, e, "alice", "doc2", "read", false) + testEnforce(t, e, "alice", "doc2", "write", false) + testEnforce(t, e, "alice", "doc3", "read", false) + testEnforce(t, e, "alice", "doc3", "write", false) + + testEnforce(t, e, "bob", "doc1", "read", false) + testEnforce(t, e, "bob", "doc1", "write", false) + testEnforce(t, e, "bob", "doc2", "read", true) + testEnforce(t, e, "bob", "doc2", "write", false) + testEnforce(t, e, "bob", "doc3", "read", false) + testEnforce(t, e, "bob", "doc3", "write", false) +} diff --git a/orbac_test.go b/orbac_test.go new file mode 100644 index 0000000..4df5ed3 --- /dev/null +++ b/orbac_test.go @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +// TestOrBACModel tests the Organization-Based Access Control (OrBAC) model. +// OrBAC extends RBAC with abstraction layers: +// - Empower (g): Maps subjects to roles within organizations +// - Use (g2): Maps concrete actions to abstract activities within organizations +// - Consider (g3): Maps concrete objects to abstract views within organizations +// - Permission (p): Grants role-activity-view permissions within organizations +// +// This separates concrete entities (subjects, actions, objects) from +// abstract security entities (roles, activities, views), allowing more +// flexible and maintainable access control policies. + +func testEnforceOrBAC(t *testing.T, e *Enforcer, sub string, org string, obj string, act string, res bool) { + t.Helper() + if myRes, err := e.Enforce(sub, org, obj, act); err != nil { + t.Errorf("Enforce Error: %s", err) + } else if myRes != res { + t.Errorf("%s, %s, %s, %s: %t, supposed to be %t", sub, org, obj, act, myRes, res) + } +} + +func TestOrBACModel(t *testing.T) { + e, err := NewEnforcer("examples/orbac_model.conf", "examples/orbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Test alice as manager in org1 - can consult (read) and modify (write) documents + testEnforceOrBAC(t, e, "alice", "org1", "data1", "read", true) + testEnforceOrBAC(t, e, "alice", "org1", "data1", "write", true) + testEnforceOrBAC(t, e, "alice", "org1", "data2", "read", true) + testEnforceOrBAC(t, e, "alice", "org1", "data2", "write", true) + + // Test bob as employee in org1 - can only consult (read) documents + testEnforceOrBAC(t, e, "bob", "org1", "data1", "read", true) + testEnforceOrBAC(t, e, "bob", "org1", "data1", "write", false) + testEnforceOrBAC(t, e, "bob", "org1", "data2", "read", true) + testEnforceOrBAC(t, e, "bob", "org1", "data2", "write", false) + + // Test charlie as manager in org2 - can consult and modify reports + testEnforceOrBAC(t, e, "charlie", "org2", "report1", "read", true) + testEnforceOrBAC(t, e, "charlie", "org2", "report1", "write", true) + testEnforceOrBAC(t, e, "charlie", "org2", "report2", "read", true) + testEnforceOrBAC(t, e, "charlie", "org2", "report2", "write", true) + + // Test david as employee in org2 - can only consult reports + testEnforceOrBAC(t, e, "david", "org2", "report1", "read", true) + testEnforceOrBAC(t, e, "david", "org2", "report1", "write", false) + testEnforceOrBAC(t, e, "david", "org2", "report2", "read", true) + testEnforceOrBAC(t, e, "david", "org2", "report2", "write", false) + + // Test cross-organization access (should be denied) + testEnforceOrBAC(t, e, "alice", "org2", "report1", "read", false) + testEnforceOrBAC(t, e, "alice", "org2", "report1", "write", false) + testEnforceOrBAC(t, e, "charlie", "org1", "data1", "read", false) + testEnforceOrBAC(t, e, "charlie", "org1", "data1", "write", false) + + // Test access to objects not in the organization's view + testEnforceOrBAC(t, e, "alice", "org1", "report1", "read", false) + testEnforceOrBAC(t, e, "charlie", "org2", "data1", "read", false) +} diff --git a/pbac_test.go b/pbac_test.go new file mode 100644 index 0000000..054c7cc --- /dev/null +++ b/pbac_test.go @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +// Helper function for PBAC enforcement testing. +func testEnforcePBAC(t *testing.T, e *Enforcer, sub interface{}, obj interface{}, act string, res bool) { + t.Helper() + myRes, err := e.Enforce(sub, obj, act) + if err != nil { + t.Errorf("Enforce Error: %s", err) + return + } + if myRes != res { + t.Errorf("%v, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res) + } +} + +func TestPBACModel(t *testing.T) { + e, _ := NewEnforcer("examples/pbac_model.conf", "examples/pbac_policy.csv") + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 25}, map[string]interface{}{"Type": "doc"}, "read", true) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 25}, map[string]interface{}{"Type": "doc"}, "read", false) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "manager", "Age": 30}, map[string]interface{}{"Type": "doc"}, "read", false) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 25}, map[string]interface{}{"Type": "doc"}, "write", false) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 25}, map[string]interface{}{"Type": "doc"}, "delete", false) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 25}, map[string]interface{}{"Type": "video"}, "read", false) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 25}, map[string]interface{}{"Type": "image"}, "read", false) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 18}, map[string]interface{}{"Type": "video"}, "play", true) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 25}, map[string]interface{}{"Type": "video"}, "play", true) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 30}, map[string]interface{}{"Type": "video"}, "play", true) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 16}, map[string]interface{}{"Type": "video"}, "play", false) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 17}, map[string]interface{}{"Type": "video"}, "play", false) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 20}, map[string]interface{}{"Type": "video"}, "read", false) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 20}, map[string]interface{}{"Type": "video"}, "write", false) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 20}, map[string]interface{}{"Type": "doc"}, "play", false) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "user", "Age": 20}, map[string]interface{}{"Type": "image"}, "play", false) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 20}, map[string]interface{}{"Type": "doc"}, "read", true) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "admin", "Age": 20}, map[string]interface{}{"Type": "video"}, "play", true) + + testEnforcePBAC(t, e, map[string]interface{}{"Role": "guest", "Age": 15}, map[string]interface{}{"Type": "secret"}, "access", false) + testEnforcePBAC(t, e, map[string]interface{}{"Role": "visitor", "Age": 25}, map[string]interface{}{"Type": "private"}, "view", false) +} diff --git a/persist/adapter.go b/persist/adapter.go new file mode 100644 index 0000000..e0d91cd --- /dev/null +++ b/persist/adapter.go @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import ( + "bytes" + "encoding/csv" + "errors" + "strings" + + "github.com/casbin/casbin/v3/model" +) + +// LoadPolicyLine loads a text line as a policy rule to model. +func LoadPolicyLine(line string, m model.Model) error { + if line == "" || strings.HasPrefix(line, "#") { + return nil + } + + r := csv.NewReader(strings.NewReader(line)) + r.Comma = ',' + r.Comment = '#' + r.TrimLeadingSpace = true + + tokens, err := r.Read() + if err != nil { + return err + } + + return LoadPolicyArray(tokens, m) +} + +// PolicyLineToCsv serializes a policy type and its rule fields into a CSV-safe line. +// Fields are separated by ", " to match the conventional layout of Casbin policy +// files, which LoadPolicyLine accepts because its reader trims leading spaces. +// Fields containing commas are properly quoted so the line can be round-tripped +// through LoadPolicyLine without corruption. +func PolicyLineToCsv(ptype string, rule []string) (string, error) { + record := append([]string{ptype}, rule...) + + // Each field is written as its own single-field record so that the standard + // library decides on quoting, then the fields are joined with ", ". + var buf bytes.Buffer + w := csv.NewWriter(&buf) + fields := make([]string, 0, len(record)) + for _, field := range record { + buf.Reset() + if err := w.Write([]string{field}); err != nil { + return "", err + } + w.Flush() + if err := w.Error(); err != nil { + return "", err + } + fields = append(fields, strings.TrimRight(buf.String(), "\r\n")) + } + + return strings.Join(fields, ", "), nil +} + +// LoadPolicyArray loads a policy rule to model. +func LoadPolicyArray(rule []string, m model.Model) error { + if len(rule) == 0 || rule[0] == "" { + return errors.New("invalid policy rule: missing policy type") + } + + key := rule[0] + sec := key[:1] + ok, err := m.HasPolicyEx(sec, key, rule[1:]) + if err != nil { + return err + } + if ok { + return nil // skip duplicated policy + } + + err = m.AddPolicy(sec, key, rule[1:]) + if err != nil { + return err + } + + return nil +} + +// Adapter is the interface for Casbin adapters. +type Adapter interface { + // LoadPolicy loads all policy rules from the storage. + LoadPolicy(model model.Model) error + // SavePolicy saves all policy rules to the storage. + SavePolicy(model model.Model) error + + // AddPolicy adds a policy rule to the storage. + // This is part of the Auto-Save feature. + AddPolicy(sec string, ptype string, rule []string) error + // RemovePolicy removes a policy rule from the storage. + // This is part of the Auto-Save feature. + RemovePolicy(sec string, ptype string, rule []string) error + // RemoveFilteredPolicy removes policy rules that match the filter from the storage. + // This is part of the Auto-Save feature. + RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error +} diff --git a/persist/adapter_context.go b/persist/adapter_context.go new file mode 100644 index 0000000..1633142 --- /dev/null +++ b/persist/adapter_context.go @@ -0,0 +1,42 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import ( + "context" + + "github.com/casbin/casbin/v3/model" +) + +// ContextAdapter provides a context-aware interface for Casbin adapters. +type ContextAdapter interface { + // LoadPolicyCtx loads all policy rules from the storage with context. + LoadPolicyCtx(ctx context.Context, model model.Model) error + // SavePolicyCtx saves all policy rules to the storage with context. + SavePolicyCtx(ctx context.Context, model model.Model) error + + // AddPolicyCtx adds a policy rule to the storage with context. + // This is part of the Auto-Save feature. + AddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error + // RemovePolicyCtx removes a policy rule from the storage with context. + // This is part of the Auto-Save feature. + RemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error + // RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context. + // This is part of the Auto-Save feature. + RemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) error +} diff --git a/persist/adapter_filtered.go b/persist/adapter_filtered.go new file mode 100644 index 0000000..dc831ea --- /dev/null +++ b/persist/adapter_filtered.go @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import ( + "github.com/casbin/casbin/v3/model" +) + +// FilteredAdapter is the interface for Casbin adapters supporting filtered policies. +type FilteredAdapter interface { + Adapter + + // LoadFilteredPolicy loads only policy rules that match the filter. + LoadFilteredPolicy(model model.Model, filter interface{}) error + // IsFiltered returns true if the loaded policy has been filtered. + IsFiltered() bool +} diff --git a/persist/adapter_filtered_context.go b/persist/adapter_filtered_context.go new file mode 100644 index 0000000..0c83c44 --- /dev/null +++ b/persist/adapter_filtered_context.go @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import ( + "context" + + "github.com/casbin/casbin/v3/model" +) + +// ContextFilteredAdapter is the context-aware interface for Casbin adapters supporting filtered policies. +type ContextFilteredAdapter interface { + ContextAdapter + + // LoadFilteredPolicyCtx loads only policy rules that match the filter. + LoadFilteredPolicyCtx(ctx context.Context, model model.Model, filter interface{}) error + // IsFilteredCtx returns true if the loaded policy has been filtered. + IsFilteredCtx(ctx context.Context) bool +} diff --git a/persist/batch_adapter.go b/persist/batch_adapter.go new file mode 100644 index 0000000..8762cac --- /dev/null +++ b/persist/batch_adapter.go @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +// BatchAdapter is the interface for Casbin adapters with multiple add and remove policy functions. +type BatchAdapter interface { + Adapter + // AddPolicies adds policy rules to the storage. + // This is part of the Auto-Save feature. + AddPolicies(sec string, ptype string, rules [][]string) error + // RemovePolicies removes policy rules from the storage. + // This is part of the Auto-Save feature. + RemovePolicies(sec string, ptype string, rules [][]string) error +} diff --git a/persist/batch_adapter_context.go b/persist/batch_adapter_context.go new file mode 100644 index 0000000..ac5595f --- /dev/null +++ b/persist/batch_adapter_context.go @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import "context" + +// ContextBatchAdapter is the context-aware interface for Casbin adapters with multiple add and remove policy functions. +type ContextBatchAdapter interface { + ContextAdapter + + // AddPoliciesCtx adds policy rules to the storage. + // This is part of the Auto-Save feature. + AddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) error + // RemovePoliciesCtx removes policy rules from the storage. + // This is part of the Auto-Save feature. + RemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) error +} diff --git a/persist/cache/cache.go b/persist/cache/cache.go new file mode 100644 index 0000000..f1fc4de --- /dev/null +++ b/persist/cache/cache.go @@ -0,0 +1,42 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package cache + +import "errors" + +var ErrNoSuchKey = errors.New("there's no such key existing in cache") + +type Cache interface { + // Set puts key and value into cache. + // First parameter for extra should be time.Time object denoting expected survival time. + // If survival time equals 0 or less, the key will always be survival. + Set(key string, value bool, extra ...interface{}) error + + // Get returns result for key, + // If there's no such key existing in cache, + // ErrNoSuchKey will be returned. + Get(key string) (bool, error) + + // Delete will remove the specific key in cache. + // If there's no such key existing in cache, + // ErrNoSuchKey will be returned. + Delete(key string) error + + // Clear deletes all the items stored in cache. + Clear() error +} diff --git a/persist/cache/cache_sync.go b/persist/cache/cache_sync.go new file mode 100644 index 0000000..c056ed2 --- /dev/null +++ b/persist/cache/cache_sync.go @@ -0,0 +1,89 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package cache + +import ( + "sync" + "time" +) + +type SyncCache struct { + cache DefaultCache + sync.RWMutex +} + +func (c *SyncCache) Set(key string, value bool, extra ...interface{}) error { + ttl := time.Duration(-1) + if len(extra) > 0 { + ttl = extra[0].(time.Duration) + } + c.Lock() + defer c.Unlock() + c.cache[key] = cacheItem{ + value: value, + expiresAt: time.Now().Add(ttl), + ttl: ttl, + } + return nil +} + +func (c *SyncCache) Get(key string) (bool, error) { + c.RLock() + res, ok := c.cache[key] + c.RUnlock() + if !ok { + return false, ErrNoSuchKey + } else { + if res.ttl > 0 && time.Now().After(res.expiresAt) { + c.Lock() + defer c.Unlock() + delete(c.cache, key) + return false, ErrNoSuchKey + } + return res.value, nil + } +} + +func (c *SyncCache) Delete(key string) error { + c.RLock() + _, ok := c.cache[key] + c.RUnlock() + if !ok { + return ErrNoSuchKey + } else { + c.Lock() + defer c.Unlock() + delete(c.cache, key) + return nil + } +} + +func (c *SyncCache) Clear() error { + c.Lock() + c.cache = make(DefaultCache) + c.Unlock() + return nil +} + +func NewSyncCache() (Cache, error) { + cache := SyncCache{ + make(DefaultCache), + sync.RWMutex{}, + } + return &cache, nil +} diff --git a/persist/cache/default-cache.go b/persist/cache/default-cache.go new file mode 100644 index 0000000..e07cfc7 --- /dev/null +++ b/persist/cache/default-cache.go @@ -0,0 +1,72 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package cache + +import "time" + +type cacheItem struct { + value bool + expiresAt time.Time + ttl time.Duration +} + +type DefaultCache map[string]cacheItem + +func (c *DefaultCache) Set(key string, value bool, extra ...interface{}) error { + ttl := time.Duration(-1) + if len(extra) > 0 { + ttl = extra[0].(time.Duration) + } + (*c)[key] = cacheItem{ + value: value, + expiresAt: time.Now().Add(ttl), + ttl: ttl, + } + return nil +} + +func (c *DefaultCache) Get(key string) (bool, error) { + if res, ok := (*c)[key]; !ok { + return false, ErrNoSuchKey + } else { + if res.ttl > 0 && time.Now().After(res.expiresAt) { + delete(*c, key) + return false, ErrNoSuchKey + } + return res.value, nil + } +} + +func (c *DefaultCache) Delete(key string) error { + if _, ok := (*c)[key]; !ok { + return ErrNoSuchKey + } else { + delete(*c, key) + return nil + } +} + +func (c *DefaultCache) Clear() error { + *c = make(DefaultCache) + return nil +} + +func NewDefaultCache() (Cache, error) { + cache := make(DefaultCache) + return &cache, nil +} diff --git a/persist/dispatcher.go b/persist/dispatcher.go new file mode 100644 index 0000000..1a46a79 --- /dev/null +++ b/persist/dispatcher.go @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +// Dispatcher is the interface for Casbin dispatcher. +type Dispatcher interface { + // AddPolicies adds policies rule to all instance. + AddPolicies(sec string, ptype string, rules [][]string) error + // RemovePolicies removes policies rule from all instance. + RemovePolicies(sec string, ptype string, rules [][]string) error + // RemoveFilteredPolicy removes policy rules that match the filter from all instance. + RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error + // ClearPolicy clears all current policy in all instances + ClearPolicy() error + // UpdatePolicy updates policy rule from all instance. + UpdatePolicy(sec string, ptype string, oldRule, newRule []string) error + // UpdatePolicies updates some policy rules from all instance + UpdatePolicies(sec string, ptype string, oldrules, newRules [][]string) error + // UpdateFilteredPolicies deletes old rules and adds new rules. + UpdateFilteredPolicies(sec string, ptype string, oldRules [][]string, newRules [][]string) error +} diff --git a/persist/file-adapter/adapter.go b/persist/file-adapter/adapter.go new file mode 100644 index 0000000..e5e6665 --- /dev/null +++ b/persist/file-adapter/adapter.go @@ -0,0 +1,157 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package fileadapter + +import ( + "bufio" + "bytes" + "errors" + "os" + "strings" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// Adapter is the file adapter for Casbin. +// It can load policy from file or save policy to file. +type Adapter struct { + filePath string +} + +func (a *Adapter) UpdatePolicy(sec string, ptype string, oldRule, newRule []string) error { + return errors.New("not implemented") +} + +func (a *Adapter) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error { + return errors.New("not implemented") +} + +func (a *Adapter) UpdateFilteredPolicies(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { + return nil, errors.New("not implemented") +} + +// NewAdapter is the constructor for Adapter. +func NewAdapter(filePath string) *Adapter { + return &Adapter{filePath: filePath} +} + +// LoadPolicy loads all policy rules from the storage. +func (a *Adapter) LoadPolicy(model model.Model) error { + if a.filePath == "" { + return errors.New("invalid file path, file path cannot be empty") + } + + return a.loadPolicyFile(model, persist.LoadPolicyLine) +} + +// SavePolicy saves all policy rules to the storage. +func (a *Adapter) SavePolicy(model model.Model) error { + if a.filePath == "" { + return errors.New("invalid file path, file path cannot be empty") + } + + var tmp bytes.Buffer + + for ptype, ast := range model["p"] { + for _, rule := range ast.Policy { + line, err := persist.PolicyLineToCsv(ptype, rule) + if err != nil { + return err + } + tmp.WriteString(line) + tmp.WriteString("\n") + } + } + + for ptype, ast := range model["g"] { + for _, rule := range ast.Policy { + line, err := persist.PolicyLineToCsv(ptype, rule) + if err != nil { + return err + } + tmp.WriteString(line) + tmp.WriteString("\n") + } + } + + return a.savePolicyFile(strings.TrimRight(tmp.String(), "\n")) +} + +func (a *Adapter) loadPolicyFile(model model.Model, handler func(string, model.Model) error) error { + f, err := os.Open(a.filePath) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + err = handler(line, model) + if err != nil { + return err + } + } + return scanner.Err() +} + +func (a *Adapter) savePolicyFile(text string) error { + f, err := os.Create(a.filePath) + if err != nil { + return err + } + w := bufio.NewWriter(f) + + _, err = w.WriteString(text) + if err != nil { + return err + } + + err = w.Flush() + if err != nil { + return err + } + + return f.Close() +} + +// AddPolicy adds a policy rule to the storage. +func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error { + return errors.New("not implemented") +} + +// AddPolicies adds policy rules to the storage. +func (a *Adapter) AddPolicies(sec string, ptype string, rules [][]string) error { + return errors.New("not implemented") +} + +// RemovePolicy removes a policy rule from the storage. +func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error { + return errors.New("not implemented") +} + +// RemovePolicies removes policy rules from the storage. +func (a *Adapter) RemovePolicies(sec string, ptype string, rules [][]string) error { + return errors.New("not implemented") +} + +// RemoveFilteredPolicy removes policy rules that match the filter from the storage. +func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { + return errors.New("not implemented") +} diff --git a/persist/file-adapter/adapter_context.go b/persist/file-adapter/adapter_context.go new file mode 100644 index 0000000..75f7e9a --- /dev/null +++ b/persist/file-adapter/adapter_context.go @@ -0,0 +1,120 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package fileadapter + +import ( + "context" + + "github.com/casbin/casbin/v3/model" +) + +func (a *Adapter) UpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.UpdatePolicy(sec, ptype, oldRule, newRule) +} + +func (a *Adapter) UpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.UpdatePolicies(sec, ptype, oldRules, newRules) +} + +func (a *Adapter) UpdateFilteredPoliciesCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { + if err := checkCtx(ctx); err != nil { + return nil, err + } + + return a.UpdateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues...) +} + +// LoadPolicyCtx loads all policy rules from the storage with context. +func (a *Adapter) LoadPolicyCtx(ctx context.Context, model model.Model) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.LoadPolicy(model) +} + +// SavePolicyCtx saves all policy rules to the storage with context. +func (a *Adapter) SavePolicyCtx(ctx context.Context, model model.Model) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.SavePolicy(model) +} + +// AddPolicyCtx adds a policy rule to the storage with context. +func (a *Adapter) AddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.AddPolicy(sec, ptype, rule) +} + +// AddPoliciesCtx adds policy rules to the storage with context. +func (a *Adapter) AddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.AddPolicies(sec, ptype, rules) +} + +// RemovePolicyCtx removes a policy rule from the storage with context. +func (a *Adapter) RemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.RemovePolicy(sec, ptype, rule) +} + +// RemovePoliciesCtx removes policy rules from the storage with context. +func (a *Adapter) RemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.RemovePolicies(sec, ptype, rules) +} + +// RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context. +func (a *Adapter) RemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) +} + +func checkCtx(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + return nil + } +} diff --git a/persist/file-adapter/adapter_filtered.go b/persist/file-adapter/adapter_filtered.go new file mode 100644 index 0000000..bda481c --- /dev/null +++ b/persist/file-adapter/adapter_filtered.go @@ -0,0 +1,159 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package fileadapter + +import ( + "bufio" + "errors" + "os" + "strings" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// FilteredAdapter is the filtered file adapter for Casbin. It can load policy +// from file or save policy to file and supports loading of filtered policies. +type FilteredAdapter struct { + *Adapter + filtered bool +} + +// Filter defines the filtering rules for a FilteredAdapter's policy. Empty values +// are ignored, but all others must match the filter. +type Filter struct { + P []string + G []string + G1 []string + G2 []string + G3 []string + G4 []string + G5 []string +} + +// NewFilteredAdapter is the constructor for FilteredAdapter. +func NewFilteredAdapter(filePath string) *FilteredAdapter { + a := FilteredAdapter{} + a.filtered = true + a.Adapter = NewAdapter(filePath) + return &a +} + +// LoadPolicy loads all policy rules from the storage. +func (a *FilteredAdapter) LoadPolicy(model model.Model) error { + a.filtered = false + return a.Adapter.LoadPolicy(model) +} + +// LoadFilteredPolicy loads only policy rules that match the filter. +func (a *FilteredAdapter) LoadFilteredPolicy(model model.Model, filter interface{}) error { + if filter == nil { + return a.LoadPolicy(model) + } + if a.filePath == "" { + return errors.New("invalid file path, file path cannot be empty") + } + + filterValue, ok := filter.(*Filter) + if !ok { + return errors.New("invalid filter type") + } + err := a.loadFilteredPolicyFile(model, filterValue, persist.LoadPolicyLine) + if err == nil { + a.filtered = true + } + return err +} + +func (a *FilteredAdapter) loadFilteredPolicyFile(model model.Model, filter *Filter, handler func(string, model.Model) error) error { + f, err := os.Open(a.filePath) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if filterLine(line, filter) { + continue + } + + err = handler(line, model) + if err != nil { + return err + } + } + return scanner.Err() +} + +// IsFiltered returns true if the loaded policy has been filtered. +func (a *FilteredAdapter) IsFiltered() bool { + return a.filtered +} + +// SavePolicy saves all policy rules to the storage. +func (a *FilteredAdapter) SavePolicy(model model.Model) error { + if a.filtered { + return errors.New("cannot save a filtered policy") + } + return a.Adapter.SavePolicy(model) +} + +func filterLine(line string, filter *Filter) bool { + if filter == nil { + return false + } + p := strings.Split(line, ",") + if len(p) == 0 { + return true + } + var filterSlice []string + switch strings.TrimSpace(p[0]) { + case "p": + filterSlice = filter.P + case "g": + filterSlice = filter.G + case "g1": + filterSlice = filter.G1 + case "g2": + filterSlice = filter.G2 + case "g3": + filterSlice = filter.G3 + case "g4": + filterSlice = filter.G4 + case "g5": + filterSlice = filter.G5 + } + return filterWords(p, filterSlice) +} + +func filterWords(line []string, filter []string) bool { + if len(line) < len(filter)+1 { + return true + } + var skipLine bool + for i, v := range filter { + if len(v) > 0 && strings.TrimSpace(v) != strings.TrimSpace(line[i+1]) { + skipLine = true + break + } + } + return skipLine +} diff --git a/persist/file-adapter/adapter_filtered_context.go b/persist/file-adapter/adapter_filtered_context.go new file mode 100644 index 0000000..89abae3 --- /dev/null +++ b/persist/file-adapter/adapter_filtered_context.go @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package fileadapter + +import ( + "context" + + "github.com/casbin/casbin/v3/model" +) + +// LoadPolicyCtx loads all policy rules from the storage with context. +func (a *FilteredAdapter) LoadPolicyCtx(ctx context.Context, model model.Model) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.LoadPolicy(model) +} + +// LoadFilteredPolicyCtx loads only policy rules that match the filter with context. +func (a *FilteredAdapter) LoadFilteredPolicyCtx(ctx context.Context, model model.Model, filter interface{}) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.LoadFilteredPolicy(model, filter) +} + +// SavePolicyCtx saves all policy rules to the storage with context. +func (a *FilteredAdapter) SavePolicyCtx(ctx context.Context, model model.Model) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.SavePolicy(model) +} + +// IsFilteredCtx returns true if the loaded policy has been filtered with context. +func (a *FilteredAdapter) IsFilteredCtx(ctx context.Context) bool { + if err := checkCtx(ctx); err != nil { + return false + } + + return a.IsFiltered() +} diff --git a/persist/file-adapter/adapter_mock.go b/persist/file-adapter/adapter_mock.go new file mode 100644 index 0000000..048bfa0 --- /dev/null +++ b/persist/file-adapter/adapter_mock.go @@ -0,0 +1,125 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package fileadapter + +import ( + "bufio" + "errors" + "io" + "os" + "strings" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// AdapterMock is the file adapter for Casbin. +// It can load policy from file or save policy to file. +type AdapterMock struct { + filePath string + errorValue string +} + +// NewAdapterMock is the constructor for AdapterMock. +func NewAdapterMock(filePath string) *AdapterMock { + a := AdapterMock{} + a.filePath = filePath + return &a +} + +// LoadPolicy loads all policy rules from the storage. +func (a *AdapterMock) LoadPolicy(model model.Model) error { + err := a.loadPolicyFile(model, persist.LoadPolicyLine) + return err +} + +// SavePolicy saves all policy rules to the storage. +func (a *AdapterMock) SavePolicy(model model.Model) error { + return nil +} + +func (a *AdapterMock) loadPolicyFile(model model.Model, handler func(string, model.Model) error) error { + f, err := os.Open(a.filePath) + if err != nil { + return err + } + defer f.Close() + + buf := bufio.NewReader(f) + for { + line, err := buf.ReadString('\n') + line = strings.TrimSpace(line) + if err2 := handler(line, model); err2 != nil { + return err2 + } + if err != nil { + if err == io.EOF { + return nil + } + return err + } + } +} + +// SetMockErr sets string to be returned by of the mock during testing. +func (a *AdapterMock) SetMockErr(errorToSet string) { + a.errorValue = errorToSet +} + +// GetMockErr returns a mock error or nil. +func (a *AdapterMock) GetMockErr() error { + var returnError error + if a.errorValue != "" { + returnError = errors.New(a.errorValue) + } + return returnError +} + +// AddPolicy adds a policy rule to the storage. +func (a *AdapterMock) AddPolicy(sec string, ptype string, rule []string) error { + return a.GetMockErr() +} + +// AddPolicies removes policy rules from the storage. +func (a *AdapterMock) AddPolicies(sec string, ptype string, rules [][]string) error { + return a.GetMockErr() +} + +// RemovePolicy removes a policy rule from the storage. +func (a *AdapterMock) RemovePolicy(sec string, ptype string, rule []string) error { + return a.GetMockErr() +} + +// RemovePolicies removes policy rules from the storage. +func (a *AdapterMock) RemovePolicies(sec string, ptype string, rules [][]string) error { + return a.GetMockErr() +} + +// UpdatePolicy removes a policy rule from the storage. +func (a *AdapterMock) UpdatePolicy(sec string, ptype string, oldRule, newPolicy []string) error { + return a.GetMockErr() +} + +func (a *AdapterMock) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error { + return a.GetMockErr() +} + +// RemoveFilteredPolicy removes policy rules that match the filter from the storage. +func (a *AdapterMock) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { + return a.GetMockErr() +} diff --git a/persist/persist_test.go b/persist/persist_test.go new file mode 100644 index 0000000..238e1c7 --- /dev/null +++ b/persist/persist_test.go @@ -0,0 +1,154 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist_test + +import ( + "reflect" + "testing" + + "github.com/casbin/casbin/v3" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +func TestPersist(t *testing.T) { + // No tests yet +} + +func testRuleCount(t *testing.T, model model.Model, expected int, sec string, ptype string, tag string) { + t.Helper() + + ruleCount := len(model[sec][ptype].Policy) + if ruleCount != expected { + t.Errorf("[%s] rule count: %d, expected %d", tag, ruleCount, expected) + } +} + +func TestDuplicateRuleInAdapter(t *testing.T) { + e, _ := casbin.NewEnforcer("../examples/basic_model.conf") + + _, _ = e.AddPolicy("alice", "data1", "read") + _, _ = e.AddPolicy("alice", "data1", "read") + + testRuleCount(t, e.GetModel(), 1, "p", "p", "AddPolicy") + + e.ClearPolicy() + + // simulate adapter.LoadPolicy with duplicate rules + _ = persist.LoadPolicyArray([]string{"p", "alice", "data1", "read"}, e.GetModel()) + _ = persist.LoadPolicyArray([]string{"p", "alice", "data1", "read"}, e.GetModel()) + + testRuleCount(t, e.GetModel(), 1, "p", "p", "LoadPolicyArray") +} + +func TestPolicyLineToCsv(t *testing.T) { + tests := []struct { + name string + ptype string + rule []string + expected string + }{ + { + // Fields are separated by ", ", the layout used by the policy files + // under examples/, so that SavePolicy does not reformat them. + name: "plain fields keep the space after the comma", + ptype: "p", + rule: []string{"alice", "data1", "read"}, + expected: "p, alice, data1, read", + }, + { + name: "field containing a comma is quoted", + ptype: "p", + rule: []string{"alice", "data1", "read", "r.attrs in ('val1','val2')"}, + expected: `p, alice, data1, read, "r.attrs in ('val1','val2')"`, + }, + { + name: "field containing a quote is escaped", + ptype: "p", + rule: []string{"alice", `say "hi"`, "read"}, + expected: `p, alice, "say ""hi""", read`, + }, + { + // A leading space must stay quoted: LoadPolicyLine trims leading + // spaces on unquoted fields. + name: "field with a leading space is quoted", + ptype: "g", + rule: []string{" alice", "admin"}, + expected: `g, " alice", admin`, + }, + { + name: "empty field", + ptype: "p", + rule: []string{"alice", "", "read"}, + expected: "p, alice, , read", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line, err := persist.PolicyLineToCsv(tt.ptype, tt.rule) + if err != nil { + t.Fatalf("PolicyLineToCsv: %v", err) + } + if line != tt.expected { + t.Errorf("line: %q, expected %q", line, tt.expected) + } + }) + } +} + +func TestPolicyLineToCsvRoundTrip(t *testing.T) { + conf := ` +[request_definition] +r = sub, obj, act, cond + +[policy_definition] +p = sub, obj, act, cond + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act +` + rule := []string{" alice", `say "hi"`, "read", "r.attrs in ('val1','val2')"} + + line, err := persist.PolicyLineToCsv("p", rule) + if err != nil { + t.Fatalf("PolicyLineToCsv: %v", err) + } + + m := model.NewModel() + if err = m.LoadModelFromText(conf); err != nil { + t.Fatalf("load model: %v", err) + } + if err = persist.LoadPolicyLine(line, m); err != nil { + t.Fatalf("LoadPolicyLine on %q: %v", line, err) + } + + rules, err := m.GetPolicy("p", "p") + if err != nil { + t.Fatalf("GetPolicy: %v", err) + } + if len(rules) != 1 { + t.Fatalf("rule count: %d, expected 1 (line: %q)", len(rules), line) + } + if !reflect.DeepEqual(rules[0], rule) { + t.Errorf("rule after round-trip: %q, expected %q (line: %q)", rules[0], rule, line) + } +} diff --git a/persist/string-adapter/adapter.go b/persist/string-adapter/adapter.go new file mode 100644 index 0000000..ef63906 --- /dev/null +++ b/persist/string-adapter/adapter.go @@ -0,0 +1,103 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package stringadapter + +import ( + "bytes" + "errors" + "strings" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// Adapter is the string adapter for Casbin. +// It can load policy from string or save policy to string. +type Adapter struct { + Line string +} + +// NewAdapter is the constructor for Adapter. +func NewAdapter(line string) *Adapter { + return &Adapter{ + Line: line, + } +} + +// LoadPolicy loads all policy rules from the storage. +func (a *Adapter) LoadPolicy(model model.Model) error { + if a.Line == "" { + return errors.New("invalid line, line cannot be empty") + } + strs := strings.Split(a.Line, "\n") + for _, str := range strs { + str = strings.TrimSpace(str) + if str == "" { + continue + } + if err := persist.LoadPolicyLine(str, model); err != nil { + return err + } + } + + return nil +} + +// SavePolicy saves all policy rules to the storage. +func (a *Adapter) SavePolicy(model model.Model) error { + var tmp bytes.Buffer + for ptype, ast := range model["p"] { + for _, rule := range ast.Policy { + line, err := persist.PolicyLineToCsv(ptype, rule) + if err != nil { + return err + } + tmp.WriteString(line) + tmp.WriteString("\n") + } + } + + for ptype, ast := range model["g"] { + for _, rule := range ast.Policy { + line, err := persist.PolicyLineToCsv(ptype, rule) + if err != nil { + return err + } + tmp.WriteString(line) + tmp.WriteString("\n") + } + } + a.Line = strings.TrimRight(tmp.String(), "\n") + return nil +} + +// AddPolicy adds a policy rule to the storage. +func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error { + return errors.New("not implemented") +} + +// RemovePolicy removes a policy rule from the storage. +func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error { + a.Line = "" + return nil +} + +// RemoveFilteredPolicy removes policy rules that match the filter from the storage. +func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { + return errors.New("not implemented") +} diff --git a/persist/string-adapter/adapter_context.go b/persist/string-adapter/adapter_context.go new file mode 100644 index 0000000..fef537f --- /dev/null +++ b/persist/string-adapter/adapter_context.go @@ -0,0 +1,78 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package stringadapter + +import ( + "context" + + "github.com/casbin/casbin/v3/model" +) + +// LoadPolicyCtx loads all policy rules from the storage with context. +func (a *Adapter) LoadPolicyCtx(ctx context.Context, model model.Model) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.LoadPolicy(model) +} + +// SavePolicyCtx saves all policy rules to the storage with context. +func (a *Adapter) SavePolicyCtx(ctx context.Context, model model.Model) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.SavePolicy(model) +} + +// AddPolicyCtx adds a policy rule to the storage with context. +func (a *Adapter) AddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.AddPolicy(sec, ptype, rule) +} + +// RemovePolicyCtx removes a policy rule from the storage with context. +func (a *Adapter) RemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.RemovePolicy(sec, ptype, rule) +} + +// RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context. +func (a *Adapter) RemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) error { + if err := checkCtx(ctx); err != nil { + return err + } + + return a.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) +} + +func checkCtx(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + return nil + } +} diff --git a/persist/string-adapter/adapter_test.go b/persist/string-adapter/adapter_test.go new file mode 100644 index 0000000..620e8f4 --- /dev/null +++ b/persist/string-adapter/adapter_test.go @@ -0,0 +1,173 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package stringadapter + +import ( + "strings" + "testing" + + "github.com/casbin/casbin/v3" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +func Test_KeyMatchRbac(t *testing.T) { + conf := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _ , _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act) +` + line := ` +p, alice, /alice_data/*, (GET)|(POST) +p, alice, /alice_data/resource1, POST +p, data_group_admin, /admin/*, POST +p, data_group_admin, /bob_data/*, POST +g, alice, data_group_admin +` + a := NewAdapter(line) + m := model.NewModel() + err := m.LoadModelFromText(conf) + if err != nil { + t.Errorf("load model from text failed: %v", err.Error()) + return + } + e, _ := casbin.NewEnforcer(m, a) + sub := "alice" + obj := "/alice_data/login" + act := "POST" + if res, _ := e.Enforce(sub, obj, act); !res { + t.Error("unexpected enforce result") + } +} + +// Test_SavePolicyRoundTripWithCommas verifies that a policy rule whose fields contain +// commas (e.g. ABAC condition expressions) survives a SavePolicy → LoadPolicy round trip +// without corruption. See https://github.com/apache/casbin/issues/1733. +func Test_SavePolicyRoundTripWithCommas(t *testing.T) { + conf := ` +[request_definition] +r = sub, obj, act, cond + +[policy_definition] +p = sub, obj, act, cond + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act +` + // cond field intentionally contains commas (as in ABAC expressions). + condWithComma := `r.attrs in ('val1','val2')` + line := "p, alice, data1, read, " + `"` + condWithComma + `"` + + a := NewAdapter(line) + m := model.NewModel() + if err := m.LoadModelFromText(conf); err != nil { + t.Fatalf("load model: %v", err) + } + e, err := casbin.NewEnforcer(m, a) + if err != nil { + t.Fatalf("new enforcer: %v", err) + } + + // SavePolicy serialises in-memory rules back to the adapter string. + err = e.SavePolicy() + if err != nil { + t.Fatalf("SavePolicy: %v", err) + } + + // The saved line must be a properly quoted CSV so that re-loading produces + // exactly one rule with the comma-containing cond field intact. + saved := a.Line + reloaded := model.NewModel() + err = reloaded.LoadModelFromText(conf) + if err != nil { + t.Fatalf("reload model: %v", err) + } + for _, l := range strings.Split(saved, "\n") { + if l == "" { + continue + } + err = persist.LoadPolicyLine(l, reloaded) + if err != nil { + t.Fatalf("LoadPolicyLine on saved line %q: %v", l, err) + } + } + + rules, err := reloaded.GetPolicy("p", "p") + if err != nil { + t.Fatalf("GetPolicy: %v", err) + } + if len(rules) != 1 { + t.Fatalf("expected 1 rule after round-trip, got %d (saved: %q)", len(rules), saved) + } + if rules[0][3] != condWithComma { + t.Errorf("cond field corrupted after round-trip: got %q, want %q", rules[0][3], condWithComma) + } +} + +func Test_StringRbac(t *testing.T) { + conf := ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _ , _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +` + line := ` +p, alice, data1, read +p, data_group_admin, data3, read +p, data_group_admin, data3, write +g, alice, data_group_admin +` + a := NewAdapter(line) + m := model.NewModel() + err := m.LoadModelFromText(conf) + if err != nil { + t.Errorf("load model from text failed: %v", err.Error()) + return + } + e, _ := casbin.NewEnforcer(m, a) + sub := "alice" // the user that wants to access a resource. + obj := "data1" // the resource that is going to be accessed. + act := "read" // the operation that the user performs on the resource. + if res, _ := e.Enforce(sub, obj, act); !res { + t.Error("unexpected enforce result") + } +} diff --git a/persist/transaction.go b/persist/transaction.go new file mode 100644 index 0000000..3d50a7d --- /dev/null +++ b/persist/transaction.go @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import "context" + +// TransactionalAdapter defines the interface for adapters that support transactions. +// Adapters implementing this interface can participate in Casbin transactions. +type TransactionalAdapter interface { + Adapter + // BeginTransaction starts a new transaction and returns a transaction context. + BeginTransaction(ctx context.Context) (TransactionContext, error) +} + +// TransactionContext represents a database transaction context. +// It provides methods to commit or rollback the transaction and get an adapter +// that operates within this transaction. +type TransactionContext interface { + // Commit commits the transaction. + Commit() error + // Rollback rolls back the transaction. + Rollback() error + // GetAdapter returns an adapter that operates within this transaction. + GetAdapter() Adapter +} + +// PolicyOperation represents a policy operation that can be buffered in a transaction. +type PolicyOperation struct { + Type OperationType // The type of operation (add, remove, update) + Section string // The section of the policy (p, g) + PolicyType string // The policy type (p, p2, g, g2, etc.) + Rules [][]string // The policy rules to operate on + OldRules [][]string // For update operations, the old rules to replace +} + +// OperationType represents the type of policy operation. +type OperationType int + +const ( + // OperationAdd represents adding policy rules. + OperationAdd OperationType = iota + // OperationRemove represents removing policy rules. + OperationRemove + // OperationUpdate represents updating policy rules. + OperationUpdate +) diff --git a/persist/update_adapter.go b/persist/update_adapter.go new file mode 100644 index 0000000..c61b160 --- /dev/null +++ b/persist/update_adapter.go @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +// UpdatableAdapter is the interface for Casbin adapters with add update policy function. +type UpdatableAdapter interface { + Adapter + // UpdatePolicy updates a policy rule from storage. + // This is part of the Auto-Save feature. + UpdatePolicy(sec string, ptype string, oldRule, newRule []string) error + // UpdatePolicies updates some policy rules to storage, like db, redis. + UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error + // UpdateFilteredPolicies deletes old rules and adds new rules. + UpdateFilteredPolicies(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) +} diff --git a/persist/update_adapter_context.go b/persist/update_adapter_context.go new file mode 100644 index 0000000..4f5817e --- /dev/null +++ b/persist/update_adapter_context.go @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import "context" + +// ContextUpdatableAdapter is the context-aware interface for Casbin adapters with add update policy function. +type ContextUpdatableAdapter interface { + ContextAdapter + + // UpdatePolicyCtx updates a policy rule from storage. + // This is part of the Auto-Save feature. + UpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) error + // UpdatePoliciesCtx updates some policy rules to storage, like db, redis. + UpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) error + // UpdateFilteredPoliciesCtx deletes old rules and adds new rules. + UpdateFilteredPoliciesCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) +} diff --git a/persist/watcher.go b/persist/watcher.go new file mode 100644 index 0000000..bcfecb5 --- /dev/null +++ b/persist/watcher.go @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +// Watcher is the interface for Casbin watchers. +type Watcher interface { + // SetUpdateCallback sets the callback function that the watcher will call + // when the policy in DB has been changed by other instances. + // A classic callback is Enforcer.LoadPolicy(). + SetUpdateCallback(func(string)) error + // Update calls the update callback of other instances to synchronize their policy. + // It is usually called after changing the policy in DB, like Enforcer.SavePolicy(), + // Enforcer.AddPolicy(), Enforcer.RemovePolicy(), etc. + Update() error + // Close stops and releases the watcher, the callback function will not be called any more. + Close() +} diff --git a/persist/watcher_ex.go b/persist/watcher_ex.go new file mode 100644 index 0000000..303f2f6 --- /dev/null +++ b/persist/watcher_ex.go @@ -0,0 +1,43 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +import "github.com/casbin/casbin/v3/model" + +// WatcherEx is the strengthened Casbin watchers. +type WatcherEx interface { + Watcher + // UpdateForAddPolicy calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.AddPolicy() + UpdateForAddPolicy(sec, ptype string, params ...string) error + // UpdateForRemovePolicy calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.RemovePolicy() + UpdateForRemovePolicy(sec, ptype string, params ...string) error + // UpdateForRemoveFilteredPolicy calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.RemoveFilteredNamedGroupingPolicy() + UpdateForRemoveFilteredPolicy(sec, ptype string, fieldIndex int, fieldValues ...string) error + // UpdateForSavePolicy calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.RemoveFilteredNamedGroupingPolicy() + UpdateForSavePolicy(model model.Model) error + // UpdateForAddPolicies calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.AddPolicies() + UpdateForAddPolicies(sec string, ptype string, rules ...[]string) error + // UpdateForRemovePolicies calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.RemovePolicies() + UpdateForRemovePolicies(sec string, ptype string, rules ...[]string) error +} diff --git a/persist/watcher_update.go b/persist/watcher_update.go new file mode 100644 index 0000000..210e02a --- /dev/null +++ b/persist/watcher_update.go @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package persist + +// UpdatableWatcher is strengthened for Casbin watchers. +type UpdatableWatcher interface { + Watcher + // UpdateForUpdatePolicy calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.UpdatePolicy() + UpdateForUpdatePolicy(sec string, ptype string, oldRule, newRule []string) error + // UpdateForUpdatePolicies calls the update callback of other instances to synchronize their policy. + // It is called after Enforcer.UpdatePolicies() + UpdateForUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error +} diff --git a/rbac/context_role_manager.go b/rbac/context_role_manager.go new file mode 100644 index 0000000..a5bb788 --- /dev/null +++ b/rbac/context_role_manager.go @@ -0,0 +1,49 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package rbac + +import "context" + +// ContextRoleManager provides a context-aware interface to define the operations for managing roles. +// Prefer this over RoleManager interface for context propagation, which is useful for things like handling +// request timeouts. +type ContextRoleManager interface { + RoleManager + + // ClearCtx clears all stored data and resets the role manager to the initial state with context. + ClearCtx(ctx context.Context) error + // AddLinkCtx adds the inheritance link between two roles. role: name1 and role: name2 with context. + // domain is a prefix to the roles (can be used for other purposes). + AddLinkCtx(ctx context.Context, name1 string, name2 string, domain ...string) error + // DeleteLinkCtx deletes the inheritance link between two roles. role: name1 and role: name2 with context. + // domain is a prefix to the roles (can be used for other purposes). + DeleteLinkCtx(ctx context.Context, name1 string, name2 string, domain ...string) error + // HasLinkCtx determines whether a link exists between two roles. role: name1 inherits role: name2 with context. + // domain is a prefix to the roles (can be used for other purposes). + HasLinkCtx(ctx context.Context, name1 string, name2 string, domain ...string) (bool, error) + // GetRolesCtx gets the roles that a user inherits with context. + // domain is a prefix to the roles (can be used for other purposes). + GetRolesCtx(ctx context.Context, name string, domain ...string) ([]string, error) + // GetUsersCtx gets the users that inherits a role with context. + // domain is a prefix to the users (can be used for other purposes). + GetUsersCtx(ctx context.Context, name string, domain ...string) ([]string, error) + // GetDomainsCtx gets domains that a user has with context. + GetDomainsCtx(ctx context.Context, name string) ([]string, error) + // GetAllDomainsCtx gets all domains with context. + GetAllDomainsCtx(ctx context.Context) ([]string, error) +} diff --git a/rbac/default-role-manager/role_manager.go b/rbac/default-role-manager/role_manager.go new file mode 100644 index 0000000..2e35d57 --- /dev/null +++ b/rbac/default-role-manager/role_manager.go @@ -0,0 +1,1252 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package defaultrolemanager + +import ( + "errors" + "sync" + + "github.com/casbin/casbin/v3/rbac" + "github.com/casbin/casbin/v3/util" +) + +const defaultDomain string = "" + +// Role represents the data structure for a role in RBAC. +type Role struct { + name string + roles *sync.Map + users *sync.Map + matched *sync.Map + matchedBy *sync.Map + linkConditionFuncMap *sync.Map + linkConditionFuncParamsMap *sync.Map +} + +func newRole(name string) *Role { + r := Role{} + r.name = name + r.roles = &sync.Map{} + r.users = &sync.Map{} + r.matched = &sync.Map{} + r.matchedBy = &sync.Map{} + r.linkConditionFuncMap = &sync.Map{} + r.linkConditionFuncParamsMap = &sync.Map{} + return &r +} + +func (r *Role) addRole(role *Role) { + r.roles.Store(role.name, role) + role.addUser(r) +} + +func (r *Role) removeRole(role *Role) { + r.roles.Delete(role.name) + role.removeUser(r) +} + +// should only be called inside addRole. +func (r *Role) addUser(user *Role) { + r.users.Store(user.name, user) +} + +// should only be called inside removeRole. +func (r *Role) removeUser(user *Role) { + r.users.Delete(user.name) +} + +func (r *Role) addMatch(role *Role) { + r.matched.Store(role.name, role) + role.matchedBy.Store(r.name, r) +} + +func (r *Role) removeMatch(role *Role) { + r.matched.Delete(role.name) + role.matchedBy.Delete(r.name) +} + +func (r *Role) removeMatches() { + r.matched.Range(func(key, value interface{}) bool { + r.removeMatch(value.(*Role)) + return true + }) + r.matchedBy.Range(func(key, value interface{}) bool { + value.(*Role).removeMatch(r) + return true + }) +} + +func (r *Role) rangeRoles(fn func(key, value interface{}) bool) { + r.roles.Range(fn) + r.roles.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.matched.Range(fn) + return true + }) + r.matchedBy.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.roles.Range(fn) + return true + }) +} + +func (r *Role) rangeUsers(fn func(key, value interface{}) bool) { + r.users.Range(fn) + r.users.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.matched.Range(fn) + return true + }) + r.matchedBy.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.users.Range(fn) + return true + }) +} + +func (r *Role) getRoles() []string { + var names []string + r.rangeRoles(func(key, value interface{}) bool { + names = append(names, key.(string)) + return true + }) + return util.RemoveDuplicateElement(names) +} + +func (r *Role) getUsers() []string { + var names []string + r.rangeUsers(func(key, value interface{}) bool { + names = append(names, key.(string)) + return true + }) + return names +} + +type linkConditionFuncKey struct { + roleName string + domainName string +} + +func (r *Role) addLinkConditionFunc(role *Role, domain string, fn rbac.LinkConditionFunc) { + r.linkConditionFuncMap.Store(linkConditionFuncKey{role.name, domain}, fn) +} + +func (r *Role) getLinkConditionFunc(role *Role, domain string) (rbac.LinkConditionFunc, bool) { + fn, ok := r.linkConditionFuncMap.Load(linkConditionFuncKey{role.name, domain}) + if fn == nil { + return nil, ok + } + return fn.(rbac.LinkConditionFunc), ok +} + +func (r *Role) setLinkConditionFuncParams(role *Role, domain string, params ...string) { + r.linkConditionFuncParamsMap.Store(linkConditionFuncKey{role.name, domain}, params) +} + +func (r *Role) getLinkConditionFuncParams(role *Role, domain string) ([]string, bool) { + params, ok := r.linkConditionFuncParamsMap.Load(linkConditionFuncKey{role.name, domain}) + if params == nil { + return nil, ok + } + return params.([]string), ok +} + +// RoleManagerImpl provides a default implementation for the RoleManager interface. +type RoleManagerImpl struct { + allRoles *sync.Map + maxHierarchyLevel int + matchingFunc rbac.MatchingFunc + domainMatchingFunc rbac.MatchingFunc + matchingFuncCache *util.SyncLRUCache + mutex sync.Mutex +} + +// NewRoleManagerImpl is the constructor for creating an instance of the +// default RoleManager implementation. +func NewRoleManagerImpl(maxHierarchyLevel int) *RoleManagerImpl { + rm := RoleManagerImpl{} + _ = rm.Clear() // init allRoles and matchingFuncCache + rm.maxHierarchyLevel = maxHierarchyLevel + return &rm +} + +// use this constructor to avoid rebuild of AddMatchingFunc. +func newRoleManagerWithMatchingFunc(maxHierarchyLevel int, fn rbac.MatchingFunc) *RoleManagerImpl { + rm := NewRoleManagerImpl(maxHierarchyLevel) + rm.matchingFunc = fn + return rm +} + +// rebuilds role cache. +func (rm *RoleManagerImpl) rebuild() { + roles := rm.allRoles + _ = rm.Clear() + rangeLinks(roles, func(name1, name2 string, domain ...string) bool { + _ = rm.AddLink(name1, name2, domain...) + return true + }) +} + +func (rm *RoleManagerImpl) Match(str string, pattern string) bool { + if str == pattern { + return true + } + + if rm.matchingFunc != nil { + return rm.matchingFunc(str, pattern) + } else { + return false + } +} + +func (rm *RoleManagerImpl) rangeMatchingRoles(name string, isPattern bool, fn func(role *Role) bool) { + rm.allRoles.Range(func(key, value interface{}) bool { + name2 := key.(string) + if isPattern && name != name2 && rm.Match(name2, name) { + fn(value.(*Role)) + } else if !isPattern && name != name2 && rm.Match(name, name2) { + fn(value.(*Role)) + } + return true + }) +} + +func (rm *RoleManagerImpl) load(name interface{}) (value *Role, ok bool) { + if r, ok := rm.allRoles.Load(name); ok { + return r.(*Role), true + } + return nil, false +} + +// loads or creates a role. +func (rm *RoleManagerImpl) getRole(name string) (r *Role, created bool) { + var role *Role + var ok bool + + if role, ok = rm.load(name); !ok { + role = newRole(name) + rm.allRoles.Store(name, role) + + if rm.matchingFunc != nil { + rm.rangeMatchingRoles(name, false, func(r *Role) bool { + r.addMatch(role) + return true + }) + + rm.rangeMatchingRoles(name, true, func(r *Role) bool { + role.addMatch(r) + return true + }) + } + } + + return role, !ok +} + +func loadAndDelete(m *sync.Map, name string) (value interface{}, loaded bool) { + value, loaded = m.Load(name) + if loaded { + m.Delete(name) + } + return value, loaded +} + +func (rm *RoleManagerImpl) removeRole(name string) { + if role, ok := loadAndDelete(rm.allRoles, name); ok { + role.(*Role).removeMatches() + } +} + +// AddMatchingFunc support use pattern in g. +func (rm *RoleManagerImpl) AddMatchingFunc(name string, fn rbac.MatchingFunc) { + rm.matchingFunc = fn + rm.rebuild() +} + +// AddDomainMatchingFunc support use domain pattern in g. +func (rm *RoleManagerImpl) AddDomainMatchingFunc(name string, fn rbac.MatchingFunc) { + rm.domainMatchingFunc = fn +} + +// Clear clears all stored data and resets the role manager to the initial state. +func (rm *RoleManagerImpl) Clear() error { + rm.matchingFuncCache = util.NewSyncLRUCache(100) + rm.allRoles = &sync.Map{} + return nil +} + +// AddLink adds the inheritance link between role: name1 and role: name2. +// aka role: name1 inherits role: name2. +func (rm *RoleManagerImpl) AddLink(name1 string, name2 string, domains ...string) error { + user, _ := rm.getRole(name1) + role, _ := rm.getRole(name2) + user.addRole(role) + return nil +} + +// DeleteLink deletes the inheritance link between role: name1 and role: name2. +// aka role: name1 does not inherit role: name2 any more. +func (rm *RoleManagerImpl) DeleteLink(name1 string, name2 string, domains ...string) error { + user, _ := rm.getRole(name1) + role, _ := rm.getRole(name2) + user.removeRole(role) + return nil +} + +// HasLink determines whether role: name1 inherits role: name2. +func (rm *RoleManagerImpl) HasLink(name1 string, name2 string, domains ...string) (bool, error) { + if name1 == name2 || (rm.matchingFunc != nil && rm.Match(name1, name2)) { + return true, nil + } + + // Lock to prevent race conditions between getRole and removeRole + rm.mutex.Lock() + defer rm.mutex.Unlock() + + user, userCreated := rm.getRole(name1) + role, roleCreated := rm.getRole(name2) + + if userCreated { + defer rm.removeRole(user.name) + } + if roleCreated { + defer rm.removeRole(role.name) + } + + return rm.hasLinkHelper(role.name, map[string]*Role{user.name: user}, rm.maxHierarchyLevel), nil +} + +func (rm *RoleManagerImpl) hasLinkHelper(targetName string, roles map[string]*Role, level int) bool { + if level < 0 || len(roles) == 0 { + return false + } + + nextRoles := map[string]*Role{} + for _, role := range roles { + if targetName == role.name || (rm.matchingFunc != nil && rm.Match(role.name, targetName)) { + return true + } + role.rangeRoles(func(key, value interface{}) bool { + nextRoles[key.(string)] = value.(*Role) + return true + }) + } + + return rm.hasLinkHelper(targetName, nextRoles, level-1) +} + +// GetRoles gets the roles that a user inherits. +func (rm *RoleManagerImpl) GetRoles(name string, domains ...string) ([]string, error) { + user, created := rm.getRole(name) + if created { + defer rm.removeRole(user.name) + } + return user.getRoles(), nil +} + +// GetUsers gets the users of a role. +// domain is an unreferenced parameter here, may be used in other implementations. +func (rm *RoleManagerImpl) GetUsers(name string, domain ...string) ([]string, error) { + role, created := rm.getRole(name) + if created { + defer rm.removeRole(role.name) + } + return role.getUsers(), nil +} + +// GetImplicitRoles gets the implicit roles that a user inherits, respecting maxHierarchyLevel. +func (rm *RoleManagerImpl) GetImplicitRoles(name string, domain ...string) ([]string, error) { + user, created := rm.getRole(name) + if created { + defer rm.removeRole(user.name) + } + + var res []string + roleSet := make(map[string]bool) + roleSet[name] = true + roles := map[string]*Role{user.name: user} + + return rm.getImplicitRolesHelper(roles, roleSet, res, 0), nil +} + +// GetImplicitUsers gets the implicit users that inherits a role, respecting maxHierarchyLevel. +func (rm *RoleManagerImpl) GetImplicitUsers(name string, domain ...string) ([]string, error) { + role, created := rm.getRole(name) + if created { + defer rm.removeRole(role.name) + } + + var res []string + userSet := make(map[string]bool) + userSet[name] = true + users := map[string]*Role{role.name: role} + + return rm.getImplicitUsersHelper(users, userSet, res, 0), nil +} + +// getImplicitRolesHelper is a helper function for GetImplicitRoles that respects maxHierarchyLevel. +func (rm *RoleManagerImpl) getImplicitRolesHelper(roles map[string]*Role, roleSet map[string]bool, res []string, level int) []string { + if level >= rm.maxHierarchyLevel || len(roles) == 0 { + return res + } + + nextRoles := map[string]*Role{} + for _, role := range roles { + role.rangeRoles(func(key, value interface{}) bool { + roleName := key.(string) + if _, ok := roleSet[roleName]; !ok { + res = append(res, roleName) + roleSet[roleName] = true + nextRoles[roleName] = value.(*Role) + } + return true + }) + } + + return rm.getImplicitRolesHelper(nextRoles, roleSet, res, level+1) +} + +// getImplicitUsersHelper is a helper function for GetImplicitUsers that respects maxHierarchyLevel. +func (rm *RoleManagerImpl) getImplicitUsersHelper(users map[string]*Role, userSet map[string]bool, res []string, level int) []string { + if level >= rm.maxHierarchyLevel || len(users) == 0 { + return res + } + + nextUsers := map[string]*Role{} + for _, user := range users { + user.rangeUsers(func(key, value interface{}) bool { + userName := key.(string) + if _, ok := userSet[userName]; !ok { + res = append(res, userName) + userSet[userName] = true + nextUsers[userName] = value.(*Role) + } + return true + }) + } + + return rm.getImplicitUsersHelper(nextUsers, userSet, res, level+1) +} + +// PrintRoles prints all the roles to log. +func (rm *RoleManagerImpl) PrintRoles() error { + // Logger has been removed - this is now a no-op + return nil +} + +// GetDomains gets domains that a user has. +func (rm *RoleManagerImpl) GetDomains(name string) ([]string, error) { + domains := []string{defaultDomain} + return domains, nil +} + +// GetAllDomains gets all domains. +func (rm *RoleManagerImpl) GetAllDomains() ([]string, error) { + domains := []string{defaultDomain} + return domains, nil +} + +func (rm *RoleManagerImpl) copyFrom(other *RoleManagerImpl) { + other.Range(func(name1, name2 string, domain ...string) bool { + _ = rm.AddLink(name1, name2, domain...) + return true + }) +} + +func rangeLinks(users *sync.Map, fn func(name1, name2 string, domain ...string) bool) { + users.Range(func(_, value interface{}) bool { + user := value.(*Role) + user.roles.Range(func(key, _ interface{}) bool { + roleName := key.(string) + return fn(user.name, roleName, defaultDomain) + }) + return true + }) +} + +func (rm *RoleManagerImpl) Range(fn func(name1, name2 string, domain ...string) bool) { + rangeLinks(rm.allRoles, fn) +} + +// Deprecated: BuildRelationship is no longer required. +func (rm *RoleManagerImpl) BuildRelationship(name1 string, name2 string, domain ...string) error { + return nil +} + +type DomainManager struct { + rmMap *sync.Map + maxHierarchyLevel int + matchingFunc rbac.MatchingFunc + domainMatchingFunc rbac.MatchingFunc + matchingFuncCache *util.SyncLRUCache +} + +// NewDomainManager is the constructor for creating an instance of the +// default DomainManager implementation. +func NewDomainManager(maxHierarchyLevel int) *DomainManager { + dm := &DomainManager{} + _ = dm.Clear() // init rmMap and rmCache + dm.maxHierarchyLevel = maxHierarchyLevel + return dm +} + +// AddMatchingFunc support use pattern in g. +func (dm *DomainManager) AddMatchingFunc(name string, fn rbac.MatchingFunc) { + dm.matchingFunc = fn + dm.rmMap.Range(func(key, value interface{}) bool { + value.(*RoleManagerImpl).AddMatchingFunc(name, fn) + return true + }) +} + +// AddDomainMatchingFunc support use domain pattern in g. +func (dm *DomainManager) AddDomainMatchingFunc(name string, fn rbac.MatchingFunc) { + dm.domainMatchingFunc = fn + dm.rmMap.Range(func(key, value interface{}) bool { + value.(*RoleManagerImpl).AddDomainMatchingFunc(name, fn) + return true + }) + dm.rebuild() +} + +// clears the map of RoleManagers. +func (dm *DomainManager) rebuild() { + rmMap := dm.rmMap + _ = dm.Clear() + rmMap.Range(func(key, value interface{}) bool { + domain := key.(string) + rm := value.(*RoleManagerImpl) + + rm.Range(func(name1, name2 string, _ ...string) bool { + _ = dm.AddLink(name1, name2, domain) + return true + }) + return true + }) +} + +// Clear clears all stored data and resets the role manager to the initial state. +func (dm *DomainManager) Clear() error { + dm.rmMap = &sync.Map{} + dm.matchingFuncCache = util.NewSyncLRUCache(100) + return nil +} + +func (dm *DomainManager) getDomain(domains ...string) (domain string, err error) { + switch len(domains) { + case 0: + return defaultDomain, nil + default: + return domains[0], nil + } +} + +func (dm *DomainManager) Match(str string, pattern string) bool { + if str == pattern { + return true + } + + if dm.domainMatchingFunc != nil { + return dm.domainMatchingFunc(str, pattern) + } else { + return false + } +} + +func (dm *DomainManager) rangeAffectedRoleManagers(domain string, fn func(rm *RoleManagerImpl)) { + if dm.domainMatchingFunc != nil { + dm.rmMap.Range(func(key, value interface{}) bool { + domain2 := key.(string) + if domain != domain2 && dm.Match(domain2, domain) { + fn(value.(*RoleManagerImpl)) + } + return true + }) + } +} + +func (dm *DomainManager) load(name interface{}) (value *RoleManagerImpl, ok bool) { + if r, ok := dm.rmMap.Load(name); ok { + return r.(*RoleManagerImpl), true + } + return nil, false +} + +// load or create a RoleManager instance of domain. +func (dm *DomainManager) getRoleManager(domain string, store bool) *RoleManagerImpl { + var rm *RoleManagerImpl + var ok bool + + if rm, ok = dm.load(domain); !ok { + rm = newRoleManagerWithMatchingFunc(dm.maxHierarchyLevel, dm.matchingFunc) + if store { + dm.rmMap.Store(domain, rm) + } + if dm.domainMatchingFunc != nil { + dm.rmMap.Range(func(key, value interface{}) bool { + domain2 := key.(string) + rm2 := value.(*RoleManagerImpl) + if domain != domain2 && dm.Match(domain, domain2) { + rm.copyFrom(rm2) + } + return true + }) + } + } + return rm +} + +// AddLink adds the inheritance link between role: name1 and role: name2. +// aka role: name1 inherits role: name2. +func (dm *DomainManager) AddLink(name1 string, name2 string, domains ...string) error { + domain, err := dm.getDomain(domains...) + if err != nil { + return err + } + roleManager := dm.getRoleManager(domain, true) // create role manager if it does not exist + _ = roleManager.AddLink(name1, name2, domains...) + + dm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.AddLink(name1, name2, domains...) + }) + return nil +} + +// DeleteLink deletes the inheritance link between role: name1 and role: name2. +// aka role: name1 does not inherit role: name2 any more. +func (dm *DomainManager) DeleteLink(name1 string, name2 string, domains ...string) error { + domain, err := dm.getDomain(domains...) + if err != nil { + return err + } + roleManager := dm.getRoleManager(domain, true) // create role manager if it does not exist + _ = roleManager.DeleteLink(name1, name2, domains...) + + dm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.DeleteLink(name1, name2, domains...) + }) + return nil +} + +// HasLink determines whether role: name1 inherits role: name2. +func (dm *DomainManager) HasLink(name1 string, name2 string, domains ...string) (bool, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return false, err + } + rm := dm.getRoleManager(domain, false) + return rm.HasLink(name1, name2, domains...) +} + +// GetRoles gets the roles that a subject inherits. +func (dm *DomainManager) GetRoles(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err + } + rm := dm.getRoleManager(domain, false) + return rm.GetRoles(name, domains...) +} + +// GetUsers gets the users of a role. +func (dm *DomainManager) GetUsers(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err + } + rm := dm.getRoleManager(domain, false) + return rm.GetUsers(name, domains...) +} + +// GetImplicitRoles gets the implicit roles that a subject inherits, respecting maxHierarchyLevel. +func (dm *DomainManager) GetImplicitRoles(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err + } + rm := dm.getRoleManager(domain, false) + return rm.GetImplicitRoles(name, domains...) +} + +// GetImplicitUsers gets the implicit users that inherits a role, respecting maxHierarchyLevel. +func (dm *DomainManager) GetImplicitUsers(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err + } + rm := dm.getRoleManager(domain, false) + return rm.GetImplicitUsers(name, domains...) +} + +// PrintRoles prints all the roles to log. +func (dm *DomainManager) PrintRoles() error { + // Logger has been removed - this is now a no-op + return nil +} + +// GetDomains gets domains that a user has. +func (dm *DomainManager) GetDomains(name string) ([]string, error) { + var domains []string + dm.rmMap.Range(func(key, value interface{}) bool { + domain := key.(string) + rm := value.(*RoleManagerImpl) + role, created := rm.getRole(name) + if created { + defer rm.removeRole(role.name) + } + if len(role.getUsers()) > 0 || len(role.getRoles()) > 0 { + domains = append(domains, domain) + } + return true + }) + return domains, nil +} + +// GetAllDomains gets all domains. +func (dm *DomainManager) GetAllDomains() ([]string, error) { + var domains []string + dm.rmMap.Range(func(key, value interface{}) bool { + domains = append(domains, key.(string)) + return true + }) + return domains, nil +} + +// Deprecated: BuildRelationship is no longer required. +func (dm *DomainManager) BuildRelationship(name1 string, name2 string, domain ...string) error { + return nil +} + +// DeleteDomain deletes the specified domain from DomainManager. +func (dm *DomainManager) DeleteDomain(domain string) error { + dm.rmMap.Delete(domain) + return nil +} + +type RoleManager struct { + *DomainManager +} + +func NewRoleManager(maxHierarchyLevel int) *RoleManager { + rm := &RoleManager{} + rm.DomainManager = NewDomainManager(maxHierarchyLevel) + return rm +} + +// DeleteDomain does nothing for RoleManagerImpl (no domain concept). +func (rm *RoleManagerImpl) DeleteDomain(domain string) error { + return errors.New("DeleteDomain is not supported by RoleManagerImpl (no domain concept)") +} + +type ConditionalRoleManager struct { + RoleManagerImpl +} + +func (crm *ConditionalRoleManager) copyFrom(other *ConditionalRoleManager) { + other.Range(func(name1, name2 string, domain ...string) bool { + _ = crm.AddLink(name1, name2, domain...) + return true + }) +} + +// use this constructor to avoid rebuild of AddMatchingFunc. +func newConditionalRoleManagerWithMatchingFunc(maxHierarchyLevel int, fn rbac.MatchingFunc) *ConditionalRoleManager { + rm := NewConditionalRoleManager(maxHierarchyLevel) + rm.matchingFunc = fn + return rm +} + +// NewConditionalRoleManager is the constructor for creating an instance of the +// ConditionalRoleManager implementation. +func NewConditionalRoleManager(maxHierarchyLevel int) *ConditionalRoleManager { + rm := ConditionalRoleManager{} + _ = rm.Clear() // init allRoles and matchingFuncCache + rm.maxHierarchyLevel = maxHierarchyLevel + return &rm +} + +// HasLink determines whether role: name1 inherits role: name2. +func (crm *ConditionalRoleManager) HasLink(name1 string, name2 string, domains ...string) (bool, error) { + if name1 == name2 || (crm.matchingFunc != nil && crm.Match(name1, name2)) { + return true, nil + } + + // Lock to prevent race conditions between getRole and removeRole + crm.mutex.Lock() + defer crm.mutex.Unlock() + + user, userCreated := crm.getRole(name1) + role, roleCreated := crm.getRole(name2) + + if userCreated { + defer crm.removeRole(user.name) + } + if roleCreated { + defer crm.removeRole(role.name) + } + + return crm.hasLinkHelper(role.name, map[string]*Role{user.name: user}, crm.maxHierarchyLevel, domains...), nil +} + +// hasLinkHelper use the Breadth First Search algorithm to traverse the Role tree +// Judging whether the user has a role (has link) is to judge whether the role node can be reached from the user node. +func (crm *ConditionalRoleManager) hasLinkHelper(targetName string, roles map[string]*Role, level int, domains ...string) bool { + if level < 0 || len(roles) == 0 { + return false + } + nextRoles := map[string]*Role{} + for _, role := range roles { + if targetName == role.name || (crm.matchingFunc != nil && crm.Match(role.name, targetName)) { + return true + } + role.rangeRoles(func(key, value interface{}) bool { + nextRole := value.(*Role) + return crm.getNextRoles(role, nextRole, domains, nextRoles) + }) + } + + return crm.hasLinkHelper(targetName, nextRoles, level-1, domains...) +} + +func (crm *ConditionalRoleManager) getNextRoles(currentRole, nextRole *Role, domains []string, nextRoles map[string]*Role) bool { + passLinkConditionFunc, err := crm.checkLinkCondition(currentRole.name, nextRole.name, domains) + if err != nil { + // Logger has been removed - error is ignored + return false + } + + if passLinkConditionFunc { + nextRoles[nextRole.name] = nextRole + } + + return true +} + +func (crm *ConditionalRoleManager) checkLinkCondition(name1, name2 string, domain []string) (bool, error) { + passLinkConditionFunc := true + var err error + + if len(domain) == 0 { + if linkConditionFunc, existLinkCondition := crm.GetLinkConditionFunc(name1, name2); existLinkCondition { + params, _ := crm.GetLinkConditionFuncParams(name1, name2) + passLinkConditionFunc, err = linkConditionFunc(params...) + } + } else { + if linkConditionFunc, existLinkCondition := crm.GetDomainLinkConditionFunc(name1, name2, domain[0]); existLinkCondition { + params, _ := crm.GetLinkConditionFuncParams(name1, name2, domain[0]) + passLinkConditionFunc, err = linkConditionFunc(params...) + } + } + + return passLinkConditionFunc, err +} + +func (crm *ConditionalRoleManager) GetRoles(name string, domains ...string) ([]string, error) { + user, created := crm.getRole(name) + if created { + defer crm.removeRole(user.name) + } + var roles []string + user.rangeRoles(func(key, value interface{}) bool { + roleName := key.(string) + passLinkConditionFunc, err := crm.checkLinkCondition(name, roleName, domains) + if err != nil { + // Logger has been removed - error is ignored + return true + } + + if passLinkConditionFunc { + roles = append(roles, roleName) + } + + return true + }) + return roles, nil +} + +func (crm *ConditionalRoleManager) GetUsers(name string, domains ...string) ([]string, error) { + role, created := crm.getRole(name) + if created { + defer crm.removeRole(name) + } + var users []string + role.rangeUsers(func(key, value interface{}) bool { + userName := key.(string) + + passLinkConditionFunc, err := crm.checkLinkCondition(userName, name, domains) + if err != nil { + // Logger has been removed - error is ignored + return true + } + + if passLinkConditionFunc { + users = append(users, userName) + } + + return true + }) + + return users, nil +} + +// GetImplicitRoles gets the implicit roles that a user inherits, respecting maxHierarchyLevel and link conditions. +func (crm *ConditionalRoleManager) GetImplicitRoles(name string, domain ...string) ([]string, error) { + user, created := crm.getRole(name) + if created { + defer crm.removeRole(user.name) + } + + var res []string + roleSet := make(map[string]bool) + roleSet[name] = true + roles := map[string]*Role{user.name: user} + + return crm.getImplicitRolesHelper(roles, roleSet, res, 0, domain), nil +} + +// GetImplicitUsers gets the implicit users that inherits a role, respecting maxHierarchyLevel and link conditions. +func (crm *ConditionalRoleManager) GetImplicitUsers(name string, domain ...string) ([]string, error) { + role, created := crm.getRole(name) + if created { + defer crm.removeRole(role.name) + } + + var res []string + userSet := make(map[string]bool) + userSet[name] = true + users := map[string]*Role{role.name: role} + + return crm.getImplicitUsersHelper(users, userSet, res, 0, domain), nil +} + +// getImplicitRolesHelper is a helper function for GetImplicitRoles that respects maxHierarchyLevel and link conditions. +func (crm *ConditionalRoleManager) getImplicitRolesHelper(roles map[string]*Role, roleSet map[string]bool, res []string, level int, domains []string) []string { + if level >= crm.maxHierarchyLevel || len(roles) == 0 { + return res + } + + nextRoles := map[string]*Role{} + for _, role := range roles { + role.rangeRoles(func(key, value interface{}) bool { + roleName := key.(string) + if _, ok := roleSet[roleName]; !ok { + passLinkConditionFunc, err := crm.checkLinkCondition(role.name, roleName, domains) + if err != nil { + // Logger has been removed - error is ignored + return true + } + + if passLinkConditionFunc { + res = append(res, roleName) + roleSet[roleName] = true + nextRoles[roleName] = value.(*Role) + } + } + return true + }) + } + + return crm.getImplicitRolesHelper(nextRoles, roleSet, res, level+1, domains) +} + +// getImplicitUsersHelper is a helper function for GetImplicitUsers that respects maxHierarchyLevel and link conditions. +func (crm *ConditionalRoleManager) getImplicitUsersHelper(users map[string]*Role, userSet map[string]bool, res []string, level int, domains []string) []string { + if level >= crm.maxHierarchyLevel || len(users) == 0 { + return res + } + + nextUsers := map[string]*Role{} + for _, user := range users { + user.rangeUsers(func(key, value interface{}) bool { + userName := key.(string) + if _, ok := userSet[userName]; !ok { + passLinkConditionFunc, err := crm.checkLinkCondition(userName, user.name, domains) + if err != nil { + // Logger has been removed - error is ignored + return true + } + + if passLinkConditionFunc { + res = append(res, userName) + userSet[userName] = true + nextUsers[userName] = value.(*Role) + } + } + return true + }) + } + + return crm.getImplicitUsersHelper(nextUsers, userSet, res, level+1, domains) +} + +// GetLinkConditionFunc get LinkConditionFunc based on userName, roleName. +func (crm *ConditionalRoleManager) GetLinkConditionFunc(userName, roleName string) (rbac.LinkConditionFunc, bool) { + return crm.GetDomainLinkConditionFunc(userName, roleName, defaultDomain) +} + +// GetDomainLinkConditionFunc get LinkConditionFunc based on userName, roleName, domain. +func (crm *ConditionalRoleManager) GetDomainLinkConditionFunc(userName, roleName, domain string) (rbac.LinkConditionFunc, bool) { + user, userCreated := crm.getRole(userName) + role, roleCreated := crm.getRole(roleName) + + if userCreated { + crm.removeRole(user.name) + return nil, false + } + + if roleCreated { + crm.removeRole(role.name) + return nil, false + } + + return user.getLinkConditionFunc(role, domain) +} + +// GetLinkConditionFuncParams gets parameters of LinkConditionFunc based on userName, roleName, domain. +func (crm *ConditionalRoleManager) GetLinkConditionFuncParams(userName, roleName string, domain ...string) ([]string, bool) { + user, userCreated := crm.getRole(userName) + role, roleCreated := crm.getRole(roleName) + + if userCreated { + crm.removeRole(user.name) + return nil, false + } + + if roleCreated { + crm.removeRole(role.name) + return nil, false + } + + domainName := defaultDomain + if len(domain) != 0 { + domainName = domain[0] + } + + if params, ok := user.getLinkConditionFuncParams(role, domainName); ok { + return params, true + } else { + return nil, false + } +} + +// AddLinkConditionFunc is based on userName, roleName, add LinkConditionFunc. +func (crm *ConditionalRoleManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) { + crm.AddDomainLinkConditionFunc(userName, roleName, defaultDomain, fn) +} + +// AddDomainLinkConditionFunc is based on userName, roleName, domain, add LinkConditionFunc. +func (crm *ConditionalRoleManager) AddDomainLinkConditionFunc(userName, roleName, domain string, fn rbac.LinkConditionFunc) { + user, _ := crm.getRole(userName) + role, _ := crm.getRole(roleName) + + user.addLinkConditionFunc(role, domain, fn) +} + +// SetLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain. +func (crm *ConditionalRoleManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) { + crm.SetDomainLinkConditionFuncParams(userName, roleName, defaultDomain, params...) +} + +// SetDomainLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain. +func (crm *ConditionalRoleManager) SetDomainLinkConditionFuncParams(userName, roleName, domain string, params ...string) { + user, _ := crm.getRole(userName) + role, _ := crm.getRole(roleName) + + user.setLinkConditionFuncParams(role, domain, params...) +} + +type ConditionalDomainManager struct { + ConditionalRoleManager + DomainManager +} + +// NewConditionalDomainManager is the constructor for creating an instance of the +// ConditionalDomainManager implementation. +func NewConditionalDomainManager(maxHierarchyLevel int) *ConditionalDomainManager { + rm := ConditionalDomainManager{} + _ = rm.Clear() // init allRoles and matchingFuncCache + rm.maxHierarchyLevel = maxHierarchyLevel + return &rm +} + +func (cdm *ConditionalDomainManager) load(name interface{}) (value *ConditionalRoleManager, ok bool) { + if r, ok := cdm.rmMap.Load(name); ok { + return r.(*ConditionalRoleManager), true + } + return nil, false +} + +// load or create a ConditionalRoleManager instance of domain. +func (cdm *ConditionalDomainManager) getConditionalRoleManager(domain string, store bool) *ConditionalRoleManager { + var rm *ConditionalRoleManager + var ok bool + + if rm, ok = cdm.load(domain); !ok { + rm = newConditionalRoleManagerWithMatchingFunc(cdm.maxHierarchyLevel, cdm.matchingFunc) + if store { + cdm.rmMap.Store(domain, rm) + } + if cdm.domainMatchingFunc != nil { + cdm.rmMap.Range(func(key, value interface{}) bool { + domain2 := key.(string) + rm2 := value.(*ConditionalRoleManager) + if domain != domain2 && cdm.Match(domain, domain2) { + rm.copyFrom(rm2) + } + return true + }) + } + } + return rm +} + +// HasLink determines whether role: name1 inherits role: name2. +func (cdm *ConditionalDomainManager) HasLink(name1 string, name2 string, domains ...string) (bool, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return false, err + } + rm := cdm.getConditionalRoleManager(domain, false) + return rm.HasLink(name1, name2, domains...) +} + +func (cdm *ConditionalDomainManager) GetRoles(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetRoles(name, domains...) +} + +func (cdm *ConditionalDomainManager) GetUsers(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetUsers(name, domains...) +} + +func (cdm *ConditionalDomainManager) GetImplicitRoles(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetImplicitRoles(name, domains...) +} + +func (cdm *ConditionalDomainManager) GetImplicitUsers(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetImplicitUsers(name, domains...) +} + +// AddLink adds the inheritance link between role: name1 and role: name2. +// aka role: name1 inherits role: name2. +func (cdm *ConditionalDomainManager) AddLink(name1 string, name2 string, domains ...string) error { + domain, err := cdm.getDomain(domains...) + if err != nil { + return err + } + conditionalRoleManager := cdm.getConditionalRoleManager(domain, true) // create role manager if it does not exist + _ = conditionalRoleManager.AddLink(name1, name2, domain) + + cdm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.AddLink(name1, name2, domain) + }) + return nil +} + +// DeleteLink deletes the inheritance link between role: name1 and role: name2. +// aka role: name1 does not inherit role: name2 any more. +func (cdm *ConditionalDomainManager) DeleteLink(name1 string, name2 string, domains ...string) error { + domain, err := cdm.getDomain(domains...) + if err != nil { + return err + } + conditionalRoleManager := cdm.getConditionalRoleManager(domain, true) // create role manager if it does not exist + _ = conditionalRoleManager.DeleteLink(name1, name2, domain) + + cdm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.DeleteLink(name1, name2, domain) + }) + return nil +} + +// AddLinkConditionFunc is based on userName, roleName, add LinkConditionFunc. +func (cdm *ConditionalDomainManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).AddLinkConditionFunc(userName, roleName, fn) + return true + }) +} + +// AddDomainLinkConditionFunc is based on userName, roleName, domain, add LinkConditionFunc. +func (cdm *ConditionalDomainManager) AddDomainLinkConditionFunc(userName, roleName, domain string, fn rbac.LinkConditionFunc) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).AddDomainLinkConditionFunc(userName, roleName, domain, fn) + return true + }) +} + +// SetLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName. +func (cdm *ConditionalDomainManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).SetLinkConditionFuncParams(userName, roleName, params...) + return true + }) +} + +// SetDomainLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain. +func (cdm *ConditionalDomainManager) SetDomainLinkConditionFuncParams(userName, roleName, domain string, params ...string) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).SetDomainLinkConditionFuncParams(userName, roleName, domain, params...) + return true + }) +} + +// AddDomainMatchingFunc support use domain pattern in g. +func (cdm *ConditionalDomainManager) AddDomainMatchingFunc(name string, fn rbac.MatchingFunc) { + cdm.domainMatchingFunc = fn + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).AddDomainMatchingFunc(name, fn) + return true + }) + cdm.rebuild() +} + +// rebuild clears the map of ConditionalRoleManagers. +func (cdm *ConditionalDomainManager) rebuild() { + rmMap := cdm.rmMap + _ = cdm.Clear() + rmMap.Range(func(key, value interface{}) bool { + domain := key.(string) + crm := value.(*ConditionalRoleManager) + + crm.Range(func(name1, name2 string, _ ...string) bool { + _ = cdm.AddLink(name1, name2, domain) + return true + }) + return true + }) +} diff --git a/rbac/default-role-manager/role_manager_test.go b/rbac/default-role-manager/role_manager_test.go new file mode 100644 index 0000000..bd5e6e0 --- /dev/null +++ b/rbac/default-role-manager/role_manager_test.go @@ -0,0 +1,436 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package defaultrolemanager + +import ( + "fmt" + "sync" + "sync/atomic" + "testing" + + "github.com/casbin/casbin/v3/rbac" + "github.com/casbin/casbin/v3/util" +) + +func testRole(t *testing.T, rm rbac.RoleManager, name1 string, name2 string, res bool) { + t.Helper() + myRes, _ := rm.HasLink(name1, name2) + t.Logf("%s, %s: %t", name1, name2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", name1, name2, !res, res) + } +} + +func testDomainRole(t *testing.T, rm rbac.RoleManager, name1 string, name2 string, domain string, res bool) { + t.Helper() + myRes, _ := rm.HasLink(name1, name2, domain) + t.Logf("%s :: %s, %s: %t", domain, name1, name2, myRes) + + if myRes != res { + t.Errorf("%s :: %s < %s: %t, supposed to be %t", domain, name1, name2, !res, res) + } +} + +func testPrintRoles(t *testing.T, rm rbac.RoleManager, name string, res []string) { + t.Helper() + myRes, _ := rm.GetRoles(name) + t.Logf("%s: %s", name, myRes) + + if !util.SetEquals(myRes, res) { + t.Errorf("%s: %s, supposed to be %s", name, myRes, res) + } +} + +func testPrintUsers(t *testing.T, rm rbac.RoleManager, name string, res []string) { + t.Helper() + myRes, _ := rm.GetUsers(name) + t.Logf("%s: %s", name, myRes) + + if !util.SetEquals(myRes, res) { + t.Errorf("%s: %s, supposed to be %s", name, myRes, res) + } +} + +func testPrintRolesWithDomain(t *testing.T, rm rbac.RoleManager, name string, domain string, res []string) { + t.Helper() + myRes, _ := rm.GetRoles(name, domain) + + if !util.SetEquals(myRes, res) { + t.Errorf("%s: %s, supposed to be %s", name, myRes, res) + } +} + +func TestRole(t *testing.T) { + rm := NewRoleManager(3) + _ = rm.AddLink("u1", "g1") + _ = rm.AddLink("u2", "g1") + _ = rm.AddLink("u3", "g2") + _ = rm.AddLink("u4", "g2") + _ = rm.AddLink("u4", "g3") + _ = rm.AddLink("g1", "g3") + + // Current role inheritance tree: + // g3 g2 + // / \ / \ + // g1 u4 u3 + // / \ + // u1 u2 + + testRole(t, rm, "u1", "g1", true) + testRole(t, rm, "u1", "g2", false) + testRole(t, rm, "u1", "g3", true) + testRole(t, rm, "u2", "g1", true) + testRole(t, rm, "u2", "g2", false) + testRole(t, rm, "u2", "g3", true) + testRole(t, rm, "u3", "g1", false) + testRole(t, rm, "u3", "g2", true) + testRole(t, rm, "u3", "g3", false) + testRole(t, rm, "u4", "g1", false) + testRole(t, rm, "u4", "g2", true) + testRole(t, rm, "u4", "g3", true) + + testPrintRoles(t, rm, "u1", []string{"g1"}) + testPrintRoles(t, rm, "u2", []string{"g1"}) + testPrintRoles(t, rm, "u3", []string{"g2"}) + testPrintRoles(t, rm, "u4", []string{"g2", "g3"}) + testPrintRoles(t, rm, "g1", []string{"g3"}) + testPrintRoles(t, rm, "g2", []string{}) + testPrintRoles(t, rm, "g3", []string{}) + + _ = rm.DeleteLink("g1", "g3") + _ = rm.DeleteLink("u4", "g2") + + // Current role inheritance tree after deleting the links: + // g3 g2 + // \ \ + // g1 u4 u3 + // / \ + // u1 u2 + + testRole(t, rm, "u1", "g1", true) + testRole(t, rm, "u1", "g2", false) + testRole(t, rm, "u1", "g3", false) + testRole(t, rm, "u2", "g1", true) + testRole(t, rm, "u2", "g2", false) + testRole(t, rm, "u2", "g3", false) + testRole(t, rm, "u3", "g1", false) + testRole(t, rm, "u3", "g2", true) + testRole(t, rm, "u3", "g3", false) + testRole(t, rm, "u4", "g1", false) + testRole(t, rm, "u4", "g2", false) + testRole(t, rm, "u4", "g3", true) + + testPrintRoles(t, rm, "u1", []string{"g1"}) + testPrintRoles(t, rm, "u2", []string{"g1"}) + testPrintRoles(t, rm, "u3", []string{"g2"}) + testPrintRoles(t, rm, "u4", []string{"g3"}) + testPrintRoles(t, rm, "g1", []string{}) + testPrintRoles(t, rm, "g2", []string{}) + testPrintRoles(t, rm, "g3", []string{}) + + rm = NewRoleManager(3) + rm.AddMatchingFunc("keyMatch", util.KeyMatch) + + _ = rm.AddLink("u1", "g1") + _ = rm.AddLink("u1", "*") + _ = rm.AddLink("u2", "g2") + + // Current role inheritance tree after deleting the links: + // g1 g2 + // \ / \ + // * u2 + // | + // u1 + testRole(t, rm, "u1", "g1", true) + testRole(t, rm, "u1", "g2", true) + testRole(t, rm, "u2", "g2", true) + testRole(t, rm, "u2", "g1", false) + testPrintRoles(t, rm, "u1", []string{"*", "u1", "u2", "g1", "g2"}) + testPrintUsers(t, rm, "*", []string{"u1"}) +} + +func TestDomainRole(t *testing.T) { + rm := NewRoleManager(3) + _ = rm.AddLink("u1", "g1", "domain1") + _ = rm.AddLink("u2", "g1", "domain1") + _ = rm.AddLink("u3", "admin", "domain2") + _ = rm.AddLink("u4", "admin", "domain2") + _ = rm.AddLink("u4", "admin", "domain1") + _ = rm.AddLink("g1", "admin", "domain1") + + // Current role inheritance tree: + // domain1:admin domain2:admin + // / \ / \ + // domain1:g1 u4 u3 + // / \ + // u1 u2 + + testDomainRole(t, rm, "u1", "g1", "domain1", true) + testDomainRole(t, rm, "u1", "g1", "domain2", false) + testDomainRole(t, rm, "u1", "admin", "domain1", true) + testDomainRole(t, rm, "u1", "admin", "domain2", false) + + testDomainRole(t, rm, "u2", "g1", "domain1", true) + testDomainRole(t, rm, "u2", "g1", "domain2", false) + testDomainRole(t, rm, "u2", "admin", "domain1", true) + testDomainRole(t, rm, "u2", "admin", "domain2", false) + + testDomainRole(t, rm, "u3", "g1", "domain1", false) + testDomainRole(t, rm, "u3", "g1", "domain2", false) + testDomainRole(t, rm, "u3", "admin", "domain1", false) + testDomainRole(t, rm, "u3", "admin", "domain2", true) + + testDomainRole(t, rm, "u4", "g1", "domain1", false) + testDomainRole(t, rm, "u4", "g1", "domain2", false) + testDomainRole(t, rm, "u4", "admin", "domain1", true) + testDomainRole(t, rm, "u4", "admin", "domain2", true) + + _ = rm.DeleteLink("g1", "admin", "domain1") + _ = rm.DeleteLink("u4", "admin", "domain2") + + // Current role inheritance tree after deleting the links: + // domain1:admin domain2:admin + // \ \ + // domain1:g1 u4 u3 + // / \ + // u1 u2 + + testDomainRole(t, rm, "u1", "g1", "domain1", true) + testDomainRole(t, rm, "u1", "g1", "domain2", false) + testDomainRole(t, rm, "u1", "admin", "domain1", false) + testDomainRole(t, rm, "u1", "admin", "domain2", false) + + testDomainRole(t, rm, "u2", "g1", "domain1", true) + testDomainRole(t, rm, "u2", "g1", "domain2", false) + testDomainRole(t, rm, "u2", "admin", "domain1", false) + testDomainRole(t, rm, "u2", "admin", "domain2", false) + + testDomainRole(t, rm, "u3", "g1", "domain1", false) + testDomainRole(t, rm, "u3", "g1", "domain2", false) + testDomainRole(t, rm, "u3", "admin", "domain1", false) + testDomainRole(t, rm, "u3", "admin", "domain2", true) + + testDomainRole(t, rm, "u4", "g1", "domain1", false) + testDomainRole(t, rm, "u4", "g1", "domain2", false) + testDomainRole(t, rm, "u4", "admin", "domain1", true) + testDomainRole(t, rm, "u4", "admin", "domain2", false) +} + +func TestClear(t *testing.T) { + rm := NewRoleManager(3) + _ = rm.AddLink("u1", "g1") + _ = rm.AddLink("u2", "g1") + _ = rm.AddLink("u3", "g2") + _ = rm.AddLink("u4", "g2") + _ = rm.AddLink("u4", "g3") + _ = rm.AddLink("g1", "g3") + + // Current role inheritance tree: + // g3 g2 + // / \ / \ + // g1 u4 u3 + // / \ + // u1 u2 + + _ = rm.Clear() + + // All data is cleared. + // No role inheritance now. + + testRole(t, rm, "u1", "g1", false) + testRole(t, rm, "u1", "g2", false) + testRole(t, rm, "u1", "g3", false) + testRole(t, rm, "u2", "g1", false) + testRole(t, rm, "u2", "g2", false) + testRole(t, rm, "u2", "g3", false) + testRole(t, rm, "u3", "g1", false) + testRole(t, rm, "u3", "g2", false) + testRole(t, rm, "u3", "g3", false) + testRole(t, rm, "u4", "g1", false) + testRole(t, rm, "u4", "g2", false) + testRole(t, rm, "u4", "g3", false) +} + +func TestDomainPatternRole(t *testing.T) { + rm := NewRoleManager(10) + rm.AddDomainMatchingFunc("keyMatch2", util.KeyMatch2) + + _ = rm.AddLink("u1", "g1", "domain1") + _ = rm.AddLink("u2", "g1", "domain2") + _ = rm.AddLink("u3", "g1", "*") + _ = rm.AddLink("u4", "g2", "domain3") + // Current role inheritance tree after deleting the links: + // domain1:g1 domain2:g1 domain3:g2 + // / \ / \ | + // domain1:u1 *:g1 domain2:u2 domain3:u4 + // | + // *:u3 + testDomainRole(t, rm, "u1", "g1", "domain1", true) + testDomainRole(t, rm, "u2", "g1", "domain1", false) + testDomainRole(t, rm, "u2", "g1", "domain2", true) + testDomainRole(t, rm, "u3", "g1", "domain1", true) + testDomainRole(t, rm, "u3", "g1", "domain2", true) + testDomainRole(t, rm, "u1", "g2", "domain1", false) + testDomainRole(t, rm, "u4", "g2", "domain3", true) + testDomainRole(t, rm, "u3", "g2", "domain3", false) + + testPrintRolesWithDomain(t, rm, "u3", "domain1", []string{"g1"}) + testPrintRolesWithDomain(t, rm, "u1", "domain1", []string{"g1"}) + testPrintRolesWithDomain(t, rm, "u3", "domain2", []string{"g1"}) + testPrintRolesWithDomain(t, rm, "u1", "domain2", []string{}) + testPrintRolesWithDomain(t, rm, "u4", "domain3", []string{"g2"}) +} + +func TestAllMatchingFunc(t *testing.T) { + rm := NewRoleManager(10) + rm.AddMatchingFunc("keyMatch2", util.KeyMatch2) + rm.AddDomainMatchingFunc("keyMatch2", util.KeyMatch2) + + _ = rm.AddLink("/book/:id", "book_group", "*") + // Current role inheritance tree after deleting the links: + // *:book_group + // | + // *:/book/:id + testDomainRole(t, rm, "/book/1", "book_group", "domain1", true) + testDomainRole(t, rm, "/book/2", "book_group", "domain1", true) +} + +func TestMatchingFuncOrder(t *testing.T) { + rm := NewRoleManager(10) + rm.AddMatchingFunc("regexMatch", util.RegexMatch) + + _ = rm.AddLink("g\\d+", "root") + _ = rm.AddLink("u1", "g1") + testRole(t, rm, "u1", "root", true) + + _ = rm.Clear() + + _ = rm.AddLink("u1", "g1") + _ = rm.AddLink("g\\d+", "root") + testRole(t, rm, "u1", "root", true) + + _ = rm.Clear() + + _ = rm.AddLink("u1", "g\\d+") + testRole(t, rm, "u1", "g1", true) + testRole(t, rm, "u1", "g2", true) +} + +func TestDomainMatchingFuncWithDifferentDomain(t *testing.T) { + rm := NewRoleManager(10) + rm.AddDomainMatchingFunc("keyMatch", util.KeyMatch) + + _ = rm.AddLink("alice", "editor", "*") + _ = rm.AddLink("editor", "admin", "domain1") + + testDomainRole(t, rm, "alice", "admin", "domain1", true) + testDomainRole(t, rm, "alice", "admin", "domain2", false) +} + +func TestTemporaryRoles(t *testing.T) { + rm := NewRoleManager(10) + rm.AddMatchingFunc("regexMatch", util.RegexMatch) + + _ = rm.AddLink("u\\d+", "user") + + for i := 0; i < 10; i++ { + testRole(t, rm, fmt.Sprintf("u%d", i), "user", true) + } + + testPrintUsers(t, rm, "user", []string{"u\\d+"}) + testPrintRoles(t, rm, "u1", []string{"user"}) + + _ = rm.AddLink("u1", "manager") + + for i := 10; i < 20; i++ { + testRole(t, rm, fmt.Sprintf("u%d", i), "user", true) + } + + testPrintUsers(t, rm, "user", []string{"u\\d+", "u1"}) + testPrintRoles(t, rm, "u1", []string{"user", "manager"}) +} + +func TestMaxHierarchyLevel(t *testing.T) { + rm := NewRoleManager(1) + _ = rm.AddLink("level0", "level1") + _ = rm.AddLink("level1", "level2") + _ = rm.AddLink("level2", "level3") + + testRole(t, rm, "level0", "level0", true) + testRole(t, rm, "level0", "level1", true) + testRole(t, rm, "level0", "level2", false) + testRole(t, rm, "level0", "level3", false) + testRole(t, rm, "level1", "level2", true) + testRole(t, rm, "level1", "level3", false) + + rm = NewRoleManager(2) + _ = rm.AddLink("level0", "level1") + _ = rm.AddLink("level1", "level2") + _ = rm.AddLink("level2", "level3") + + testRole(t, rm, "level0", "level0", true) + testRole(t, rm, "level0", "level1", true) + testRole(t, rm, "level0", "level2", true) + testRole(t, rm, "level0", "level3", false) + testRole(t, rm, "level1", "level2", true) + testRole(t, rm, "level1", "level3", true) +} + +// TestConcurrentHasLink tests concurrent HasLink calls for race conditions. +// This test verifies that concurrent HasLink calls with matching functions +// don't produce inconsistent results due to temporary role creation/deletion races. +// Regression test for issue #1318. +func TestConcurrentHasLink(t *testing.T) { + rm := NewRoleManager(10) + rm.AddMatchingFunc("keyMatch2", util.KeyMatch2) + + _ = rm.AddLink("alice", "admin") + _ = rm.AddLink("admin", "/data/*") + + expected, _ := rm.HasLink("alice", "/data/123") + + const numGoroutines = 20 + const numIterations = 100 + + var inconsistencies int64 + var wg sync.WaitGroup + + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < numIterations; j++ { + result, err := rm.HasLink("alice", "/data/123") + if err != nil { + t.Errorf("HasLink failed: %v", err) + return + } else if result != expected { + atomic.AddInt64(&inconsistencies, 1) + } + } + }() + } + + wg.Wait() + + if inconsistencies > 0 { + t.Errorf("Found %d inconsistencies in %d total operations", + inconsistencies, numGoroutines*numIterations) + } +} diff --git a/rbac/role_manager.go b/rbac/role_manager.go new file mode 100644 index 0000000..ef5f3b5 --- /dev/null +++ b/rbac/role_manager.go @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package rbac + +type MatchingFunc func(arg1 string, arg2 string) bool + +type LinkConditionFunc = func(args ...string) (bool, error) + +// RoleManager provides interface to define the operations for managing roles. +type RoleManager interface { + // Clear clears all stored data and resets the role manager to the initial state. + Clear() error + // AddLink adds the inheritance link between two roles. role: name1 and role: name2. + // domain is a prefix to the roles (can be used for other purposes). + AddLink(name1 string, name2 string, domain ...string) error + // Deprecated: BuildRelationship is no longer required + BuildRelationship(name1 string, name2 string, domain ...string) error + // DeleteLink deletes the inheritance link between two roles. role: name1 and role: name2. + // domain is a prefix to the roles (can be used for other purposes). + DeleteLink(name1 string, name2 string, domain ...string) error + // HasLink determines whether a link exists between two roles. role: name1 inherits role: name2. + // domain is a prefix to the roles (can be used for other purposes). + HasLink(name1 string, name2 string, domain ...string) (bool, error) + // GetRoles gets the roles that a user inherits. + // domain is a prefix to the roles (can be used for other purposes). + GetRoles(name string, domain ...string) ([]string, error) + // GetUsers gets the users that inherits a role. + // domain is a prefix to the users (can be used for other purposes). + GetUsers(name string, domain ...string) ([]string, error) + // GetImplicitRoles gets the implicit roles that a user inherits, respecting maxHierarchyLevel. + // domain is a prefix to the roles (can be used for other purposes). + GetImplicitRoles(name string, domain ...string) ([]string, error) + // GetImplicitUsers gets the implicit users that inherits a role, respecting maxHierarchyLevel. + // domain is a prefix to the users (can be used for other purposes). + GetImplicitUsers(name string, domain ...string) ([]string, error) + // GetDomains gets domains that a user has + GetDomains(name string) ([]string, error) + // GetAllDomains gets all domains + GetAllDomains() ([]string, error) + // PrintRoles prints all the roles to log. + PrintRoles() error + // Match matches the domain with the pattern + Match(str string, pattern string) bool + // AddMatchingFunc adds the matching function + AddMatchingFunc(name string, fn MatchingFunc) + // AddDomainMatchingFunc adds the domain matching function + AddDomainMatchingFunc(name string, fn MatchingFunc) + // DeleteDomain deletes all data of a domain in the role manager. + DeleteDomain(domain string) error +} + +// ConditionalRoleManager provides interface to define the operations for managing roles. +// Link with conditions is supported. +type ConditionalRoleManager interface { + RoleManager + + // AddLinkConditionFunc Add condition function fn for Link userName->roleName, + // when fn returns true, Link is valid, otherwise invalid + AddLinkConditionFunc(userName, roleName string, fn LinkConditionFunc) + // SetLinkConditionFuncParams Sets the parameters of the condition function fn for Link userName->roleName + SetLinkConditionFuncParams(userName, roleName string, params ...string) + // AddDomainLinkConditionFunc Add condition function fn for Link userName-> {roleName, domain}, + // when fn returns true, Link is valid, otherwise invalid + AddDomainLinkConditionFunc(user string, role string, domain string, fn LinkConditionFunc) + // SetDomainLinkConditionFuncParams Sets the parameters of the condition function fn + // for Link userName->{roleName, domain} + SetDomainLinkConditionFuncParams(user string, role string, domain string, params ...string) +} diff --git a/rbac_api.go b/rbac_api.go new file mode 100644 index 0000000..a130491 --- /dev/null +++ b/rbac_api.go @@ -0,0 +1,733 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + "strings" + + "github.com/casbin/casbin/v3/rbac" + + "github.com/casbin/casbin/v3/constant" + "github.com/casbin/casbin/v3/errors" + "github.com/casbin/casbin/v3/util" +) + +// GetRolesForUser gets the roles that a user has. +func (e *Enforcer) GetRolesForUser(name string, domain ...string) ([]string, error) { + rm := e.GetRoleManager() + if rm == nil { + return nil, fmt.Errorf("role manager is not initialized") + } + res, err := rm.GetRoles(name, domain...) + return res, err +} + +// GetUsersForRole gets the users that has a role. +func (e *Enforcer) GetUsersForRole(name string, domain ...string) ([]string, error) { + rm := e.GetRoleManager() + if rm == nil { + return nil, fmt.Errorf("role manager is not initialized") + } + res, err := rm.GetUsers(name, domain...) + return res, err +} + +// HasRoleForUser determines whether a user has a role. +func (e *Enforcer) HasRoleForUser(name string, role string, domain ...string) (bool, error) { + roles, err := e.GetRolesForUser(name, domain...) + if err != nil { + return false, err + } + hasRole := false + for _, r := range roles { + if r == role { + hasRole = true + break + } + } + + return hasRole, nil +} + +// AddRoleForUser adds a role for a user. +// Returns false if the user already has the role (aka not affected). +func (e *Enforcer) AddRoleForUser(user string, role string, domain ...string) (bool, error) { + args := []string{user, role} + args = append(args, domain...) + return e.AddGroupingPolicy(args) +} + +// AddRolesForUser adds roles for a user. +// Returns false if the user already has the roles (aka not affected). +func (e *Enforcer) AddRolesForUser(user string, roles []string, domain ...string) (bool, error) { + var rules [][]string + for _, role := range roles { + rule := []string{user, role} + rule = append(rule, domain...) + rules = append(rules, rule) + } + return e.AddGroupingPolicies(rules) +} + +// DeleteRoleForUser deletes a role for a user. +// Returns false if the user does not have the role (aka not affected). +func (e *Enforcer) DeleteRoleForUser(user string, role string, domain ...string) (bool, error) { + args := []string{user, role} + args = append(args, domain...) + return e.RemoveGroupingPolicy(args) +} + +// DeleteRolesForUser deletes all roles for a user. +// Returns false if the user does not have any roles (aka not affected). +func (e *Enforcer) DeleteRolesForUser(user string, domain ...string) (bool, error) { + var args []string + if len(domain) == 0 { + args = []string{user} + } else if len(domain) > 1 { + return false, errors.ErrDomainParameter + } else { + args = []string{user, "", domain[0]} + } + return e.RemoveFilteredGroupingPolicy(0, args...) +} + +// DeleteUser deletes a user. +// Returns false if the user does not exist (aka not affected). +func (e *Enforcer) DeleteUser(user string) (bool, error) { + var err error + res1, err := e.RemoveFilteredGroupingPolicy(0, user) + if err != nil { + return res1, err + } + + subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex) + if err != nil { + return false, err + } + res2, err := e.RemoveFilteredPolicy(subIndex, user) + return res1 || res2, err +} + +// DeleteRole deletes a role. +// Returns false if the role does not exist (aka not affected). +func (e *Enforcer) DeleteRole(role string) (bool, error) { + var err error + res1, err := e.RemoveFilteredGroupingPolicy(0, role) + if err != nil { + return res1, err + } + + res2, err := e.RemoveFilteredGroupingPolicy(1, role) + if err != nil { + return res1, err + } + + subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex) + if err != nil { + return false, err + } + res3, err := e.RemoveFilteredPolicy(subIndex, role) + return res1 || res2 || res3, err +} + +// DeletePermission deletes a permission. +// Returns false if the permission does not exist (aka not affected). +func (e *Enforcer) DeletePermission(permission ...string) (bool, error) { + return e.RemoveFilteredPolicy(1, permission...) +} + +// AddPermissionForUser adds a permission for a user or role. +// Returns false if the user or role already has the permission (aka not affected). +func (e *Enforcer) AddPermissionForUser(user string, permission ...string) (bool, error) { + return e.AddPolicy(util.JoinSlice(user, permission...)) +} + +// AddPermissionsForUser adds multiple permissions for a user or role. +// Returns false if the user or role already has one of the permissions (aka not affected). +func (e *Enforcer) AddPermissionsForUser(user string, permissions ...[]string) (bool, error) { + var rules [][]string + for _, permission := range permissions { + rules = append(rules, util.JoinSlice(user, permission...)) + } + return e.AddPolicies(rules) +} + +// DeletePermissionForUser deletes a permission for a user or role. +// Returns false if the user or role does not have the permission (aka not affected). +func (e *Enforcer) DeletePermissionForUser(user string, permission ...string) (bool, error) { + return e.RemovePolicy(util.JoinSlice(user, permission...)) +} + +// DeletePermissionsForUser deletes permissions for a user or role. +// Returns false if the user or role does not have any permissions (aka not affected). +func (e *Enforcer) DeletePermissionsForUser(user string) (bool, error) { + subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex) + if err != nil { + return false, err + } + return e.RemoveFilteredPolicy(subIndex, user) +} + +// GetPermissionsForUser gets permissions for a user or role. +func (e *Enforcer) GetPermissionsForUser(user string, domain ...string) ([][]string, error) { + return e.GetNamedPermissionsForUser("p", user, domain...) +} + +// GetNamedPermissionsForUser gets permissions for a user or role by named policy. +func (e *Enforcer) GetNamedPermissionsForUser(ptype string, user string, domain ...string) ([][]string, error) { + permission := make([][]string, 0) + for pType, assertion := range e.model["p"] { + if pType != ptype { + continue + } + args := make([]string, len(assertion.Tokens)) + subIndex, err := e.GetFieldIndex(ptype, constant.SubjectIndex) + if err != nil { + subIndex = 0 + } + args[subIndex] = user + + if len(domain) > 0 { + var index int + index, err = e.GetFieldIndex(ptype, constant.DomainIndex) + if err != nil { + return permission, err + } + args[index] = domain[0] + } + perm, err := e.GetFilteredNamedPolicy(ptype, 0, args...) + if err != nil { + return permission, err + } + permission = append(permission, perm...) + } + return permission, nil +} + +// HasPermissionForUser determines whether a user has a permission. +func (e *Enforcer) HasPermissionForUser(user string, permission ...string) (bool, error) { + return e.HasPolicy(util.JoinSlice(user, permission...)) +} + +// GetImplicitRolesForUser gets implicit roles that a user has. +// Compared to GetRolesForUser(), this function retrieves indirect roles besides direct roles. +// For example: +// g, alice, role:admin +// g, role:admin, role:user +// +// GetRolesForUser("alice") can only get: ["role:admin"]. +// But GetImplicitRolesForUser("alice") will get: ["role:admin", "role:user"]. +func (e *Enforcer) GetImplicitRolesForUser(name string, domain ...string) ([]string, error) { + var res []string + + for rm := range e.rmMap { + roles, err := e.GetNamedImplicitRolesForUser(rm, name, domain...) + if err != nil { + return nil, err + } + res = append(res, roles...) + } + + for crm := range e.condRmMap { + roles, err := e.GetNamedImplicitRolesForUser(crm, name, domain...) + if err != nil { + return nil, err + } + res = append(res, roles...) + } + + return res, nil +} + +// GetNamedImplicitRolesForUser gets implicit roles that a user has by named role definition. +// Compared to GetImplicitRolesForUser(), this function retrieves indirect roles besides direct roles. +// For example: +// g, alice, role:admin +// g, role:admin, role:user +// g2, alice, role:admin2 +// +// GetImplicitRolesForUser("alice") can only get: ["role:admin", "role:user"]. +// But GetNamedImplicitRolesForUser("g2", "alice") will get: ["role:admin2"]. +func (e *Enforcer) GetNamedImplicitRolesForUser(ptype string, name string, domain ...string) ([]string, error) { + rm := e.GetNamedRoleManager(ptype) + if rm == nil { + return nil, fmt.Errorf("role manager %s is not initialized", ptype) + } + + // Use the role manager's GetImplicitRoles method which respects maxHierarchyLevel + return rm.GetImplicitRoles(name, domain...) +} + +// GetImplicitUsersForRole gets implicit users for a role. +func (e *Enforcer) GetImplicitUsersForRole(name string, domain ...string) ([]string, error) { + res := []string{} + var rms []rbac.RoleManager + + for _, rm := range e.rmMap { + rms = append(rms, rm) + } + for _, crm := range e.condRmMap { + rms = append(rms, crm) + } + + for _, rm := range rms { + // Use the role manager's GetImplicitUsers method which respects maxHierarchyLevel + users, err := rm.GetImplicitUsers(name, domain...) + if err != nil && err.Error() != "error: name does not exist" { + return nil, err + } + res = append(res, users...) + } + + return res, nil +} + +// GetImplicitPermissionsForUser gets implicit permissions for a user or role. +// Compared to GetPermissionsForUser(), this function retrieves permissions for inherited roles. +// For example: +// p, admin, data1, read +// p, alice, data2, read +// g, alice, admin +// +// GetPermissionsForUser("alice") can only get: [["alice", "data2", "read"]]. +// But GetImplicitPermissionsForUser("alice") will get: [["admin", "data1", "read"], ["alice", "data2", "read"]]. +func (e *Enforcer) GetImplicitPermissionsForUser(user string, domain ...string) ([][]string, error) { + return e.GetNamedImplicitPermissionsForUser("p", "g", user, domain...) +} + +// GetNamedImplicitPermissionsForUser gets implicit permissions for a user or role by named policy. +// Compared to GetNamedPermissionsForUser(), this function retrieves permissions for inherited roles. +// For example: +// p, admin, data1, read +// p2, admin, create +// g, alice, admin +// +// GetImplicitPermissionsForUser("alice") can only get: [["admin", "data1", "read"]], whose policy is default policy "p" +// But you can specify the named policy "p2" to get: [["admin", "create"]] by GetNamedImplicitPermissionsForUser("p2","alice"). +func (e *Enforcer) GetNamedImplicitPermissionsForUser(ptype string, gtype string, user string, domain ...string) ([][]string, error) { + permission := make([][]string, 0) + rm := e.GetNamedRoleManager(gtype) + if rm == nil { + return nil, fmt.Errorf("role manager %s is not initialized", gtype) + } + + roles, err := e.GetNamedImplicitRolesForUser(gtype, user, domain...) + if err != nil { + return nil, err + } + policyRoles := make(map[string]struct{}, len(roles)+1) + policyRoles[user] = struct{}{} + for _, r := range roles { + policyRoles[r] = struct{}{} + } + + domainIndex, err := e.GetFieldIndex(ptype, constant.DomainIndex) + for _, rule := range e.model["p"][ptype].Policy { + if len(domain) == 0 { + if _, ok := policyRoles[rule[0]]; ok { + permission = append(permission, deepCopyPolicy(rule)) + } + continue + } + if len(domain) > 1 { + return nil, errors.ErrDomainParameter + } + if err != nil { + return nil, err + } + d := domain[0] + matched := rm.Match(d, rule[domainIndex]) + if !matched { + continue + } + if _, ok := policyRoles[rule[0]]; ok { + newRule := deepCopyPolicy(rule) + newRule[domainIndex] = d + permission = append(permission, newRule) + } + } + return permission, nil +} + +// GetImplicitUsersForPermission gets implicit users for a permission. +// For example: +// p, admin, data1, read +// p, bob, data1, read +// g, alice, admin +// +// GetImplicitUsersForPermission("data1", "read") will get: ["alice", "bob"]. +// Note: only users will be returned, roles (2nd arg in "g") will be excluded. +func (e *Enforcer) GetImplicitUsersForPermission(permission ...string) ([]string, error) { + pSubjects, err := e.GetAllSubjects() + if err != nil { + return nil, err + } + gInherit, err := e.model.GetValuesForFieldInPolicyAllTypes("g", 1) + if err != nil { + return nil, err + } + gSubjects, err := e.model.GetValuesForFieldInPolicyAllTypes("g", 0) + if err != nil { + return nil, err + } + + subjects := append(pSubjects, gSubjects...) + util.ArrayRemoveDuplicates(&subjects) + + subjects = util.SetSubtract(subjects, gInherit) + + res := []string{} + for _, user := range subjects { + req := util.JoinSliceAny(user, permission...) + allowed, err := e.Enforce(req...) + if err != nil { + return nil, err + } + + if allowed { + res = append(res, user) + } + } + + return res, nil +} + +// GetDomainsForUser gets all domains. +func (e *Enforcer) GetDomainsForUser(user string) ([]string, error) { + var domains []string + for _, rm := range e.rmMap { + domain, err := rm.GetDomains(user) + if err != nil { + return nil, err + } + domains = append(domains, domain...) + } + for _, crm := range e.condRmMap { + domain, err := crm.GetDomains(user) + if err != nil { + return nil, err + } + domains = append(domains, domain...) + } + return domains, nil +} + +// GetImplicitResourcesForUser returns all policies that user obtaining in domain. +func (e *Enforcer) GetImplicitResourcesForUser(user string, domain ...string) ([][]string, error) { + permissions, err := e.GetImplicitPermissionsForUser(user, domain...) + if err != nil { + return nil, err + } + res := make([][]string, 0) + for _, permission := range permissions { + if permission[0] == user { + res = append(res, permission) + continue + } + resLocal := [][]string{{user}} + tokensLength := len(permission) + t := make([][]string, 1, tokensLength) + for _, token := range permission[1:] { + tokens, err := e.GetImplicitUsersForRole(token, domain...) + if err != nil { + return nil, err + } + tokens = append(tokens, token) + t = append(t, tokens) + } + for i := 1; i < tokensLength; i++ { + n := make([][]string, 0) + for _, tokens := range t[i] { + for _, policy := range resLocal { + t := append([]string(nil), policy...) + t = append(t, tokens) + n = append(n, t) + } + } + resLocal = n + } + res = append(res, resLocal...) + } + return res, nil +} + +// deepCopyPolicy returns a deepcopy version of the policy to prevent changing policies through returned slice. +func deepCopyPolicy(src []string) []string { + newRule := make([]string, len(src)) + copy(newRule, src) + return newRule +} + +// GetAllowedObjectConditions returns a string array of object conditions that the user can access. +// For example: conditions, err := e.GetAllowedObjectConditions("alice", "read", "r.obj.") +// Note: +// +// 0. prefix: You can customize the prefix of the object conditions, and "r.obj." is commonly used as a prefix. +// After removing the prefix, the remaining part is the condition of the object. +// If there is an obj policy that does not meet the prefix requirement, an errors.ERR_OBJ_CONDITION will be returned. +// +// 1. If the 'objectConditions' array is empty, return errors.ERR_EMPTY_CONDITION +// This error is returned because some data adapters' ORM return full table data by default +// when they receive an empty condition, which tends to behave contrary to expectations.(e.g. GORM) +// If you are using an adapter that does not behave like this, you can choose to ignore this error. +func (e *Enforcer) GetAllowedObjectConditions(user string, action string, prefix string) ([]string, error) { + permissions, err := e.GetImplicitPermissionsForUser(user) + if err != nil { + return nil, err + } + + var objectConditions []string + for _, policy := range permissions { + // policy {sub, obj, act} + if policy[2] == action { + if !strings.HasPrefix(policy[1], prefix) { + return nil, errors.ErrObjCondition + } + objectConditions = append(objectConditions, strings.TrimPrefix(policy[1], prefix)) + } + } + + if len(objectConditions) == 0 { + return nil, errors.ErrEmptyCondition + } + + return objectConditions, nil +} + +// removeDuplicatePermissions Convert permissions to string as a hash to deduplicate. +func removeDuplicatePermissions(permissions [][]string) [][]string { + permissionsSet := make(map[string]bool) + res := make([][]string, 0) + for _, permission := range permissions { + permissionStr := util.ArrayToString(permission) + if permissionsSet[permissionStr] { + continue + } + permissionsSet[permissionStr] = true + res = append(res, permission) + } + return res +} + +// GetImplicitUsersForResource return implicit user based on resource. +// for example: +// p, alice, data1, read +// p, bob, data2, write +// p, data2_admin, data2, read +// p, data2_admin, data2, write +// g, alice, data2_admin +// GetImplicitUsersForResource("data2") will return [[bob data2 write] [alice data2 read] [alice data2 write]] +// GetImplicitUsersForResource("data1") will return [[alice data1 read]] +// Note: only users will be returned, roles (2nd arg in "g") will be excluded. +func (e *Enforcer) GetImplicitUsersForResource(resource string) ([][]string, error) { + return e.GetNamedImplicitUsersForResource("g", resource) +} + +// GetNamedImplicitUsersForResource return implicit user based on resource with named policy support. +// This function handles resource role relationships through named policies (e.g., g2, g3, etc.). +// for example: +// p, admin_group, admin_data, * +// g, admin, admin_group +// g2, app, admin_data +// GetNamedImplicitUsersForResource("g2", "app") will return users who have access to admin_data through g2 relationship. +func (e *Enforcer) GetNamedImplicitUsersForResource(ptype string, resource string) ([][]string, error) { + permissions := make([][]string, 0) + subjectIndex, _ := e.GetFieldIndex("p", "sub") + objectIndex, _ := e.GetFieldIndex("p", "obj") + rm := e.GetRoleManager() + if rm == nil { + return nil, fmt.Errorf("role manager is not initialized") + } + + isRole := make(map[string]bool) + roles, err := e.GetAllRoles() + if err != nil { + return nil, err + } + for _, role := range roles { + isRole[role] = true + } + + // Get all resource types that the resource can access through ptype (e.g., g2) + ptypePolicies, _ := e.GetNamedGroupingPolicy(ptype) + resourceAccessibleResourceTypes := make(map[string]bool) + + for _, ptypePolicy := range ptypePolicies { + if ptypePolicy[0] == resource { // ptypePolicy[0] is the resource + resourceAccessibleResourceTypes[ptypePolicy[1]] = true // ptypePolicy[1] is the resource type it can access + } + } + + for _, rule := range e.model["p"]["p"].Policy { + obj := rule[objectIndex] + sub := rule[subjectIndex] + + // Check if this policy is directly for the resource OR for a resource type the resource can access + if obj == resource || resourceAccessibleResourceTypes[obj] { + if !isRole[sub] { + permissions = append(permissions, rule) + } else { + users, err := rm.GetUsers(sub) + if err != nil { + continue + } + + for _, user := range users { + implicitUserRule := deepCopyPolicy(rule) + implicitUserRule[subjectIndex] = user + permissions = append(permissions, implicitUserRule) + } + } + } + } + + res := removeDuplicatePermissions(permissions) + return res, nil +} + +// GetImplicitUsersForResourceByDomain return implicit user based on resource and domain. +// Compared to GetImplicitUsersForResource, domain is supported. +func (e *Enforcer) GetImplicitUsersForResourceByDomain(resource string, domain string) ([][]string, error) { + permissions := make([][]string, 0) + subjectIndex, _ := e.GetFieldIndex("p", "sub") + objectIndex, _ := e.GetFieldIndex("p", "obj") + domIndex, _ := e.GetFieldIndex("p", "dom") + rm := e.GetRoleManager() + if rm == nil { + return nil, fmt.Errorf("role manager is not initialized") + } + + isRole := make(map[string]bool) + + if roles, err := e.GetAllRolesByDomain(domain); err != nil { + return nil, err + } else { + for _, role := range roles { + isRole[role] = true + } + } + + for _, rule := range e.model["p"]["p"].Policy { + obj := rule[objectIndex] + if obj != resource { + continue + } + + sub := rule[subjectIndex] + + if !isRole[sub] { + permissions = append(permissions, rule) + } else { + if domain != rule[domIndex] { + continue + } + users, err := rm.GetUsers(sub, domain) + if err != nil { + return nil, err + } + + for _, user := range users { + implicitUserRule := deepCopyPolicy(rule) + implicitUserRule[subjectIndex] = user + permissions = append(permissions, implicitUserRule) + } + } + } + + res := removeDuplicatePermissions(permissions) + return res, nil +} + +// GetImplicitObjectPatternsForUser returns all object patterns (with wildcards) that a user has for a given domain and action. +// For example: +// p, admin, chronicle/123, location/*, read +// p, user, chronicle/456, location/789, read +// g, alice, admin +// g, bob, user +// +// GetImplicitObjectPatternsForUser("alice", "chronicle/123", "read") will return ["location/*"]. +// GetImplicitObjectPatternsForUser("bob", "chronicle/456", "read") will return ["location/789"]. +func (e *Enforcer) GetImplicitObjectPatternsForUser(user string, domain string, action string) ([]string, error) { + roles, err := e.GetImplicitRolesForUser(user, domain) + if err != nil { + return nil, err + } + + subjects := append([]string{user}, roles...) + subjectIndex, _ := e.GetFieldIndex("p", constant.SubjectIndex) + domainIndex, _ := e.GetFieldIndex("p", constant.DomainIndex) + objectIndex, _ := e.GetFieldIndex("p", constant.ObjectIndex) + actionIndex, _ := e.GetFieldIndex("p", constant.ActionIndex) + + patterns := make(map[string]struct{}) + for _, rule := range e.model["p"]["p"].Policy { + sub := rule[subjectIndex] + matched := false + for _, subject := range subjects { + if sub == subject { + matched = true + break + } + } + if !matched { + continue + } + + if !e.matchDomain(domainIndex, domain, rule) { + continue + } + + ruleAction := rule[actionIndex] + if ruleAction != action && ruleAction != "*" { + continue + } + + obj := rule[objectIndex] + patterns[obj] = struct{}{} + } + + result := make([]string, 0, len(patterns)) + for pattern := range patterns { + result = append(result, pattern) + } + + return result, nil +} + +// matchDomain checks if the domain matches the rule domain using pattern matching. +func (e *Enforcer) matchDomain(domainIndex int, domain string, rule []string) bool { + if domainIndex < 0 || domain == "" { + return true + } + ruleDomain := rule[domainIndex] + if ruleDomain == domain { + return true + } + for _, rm := range e.rmMap { + if rm.Match(domain, ruleDomain) { + return true + } + } + for _, crm := range e.condRmMap { + if crm.Match(domain, ruleDomain) { + return true + } + } + return false +} diff --git a/rbac_api_context.go b/rbac_api_context.go new file mode 100644 index 0000000..253f2df --- /dev/null +++ b/rbac_api_context.go @@ -0,0 +1,134 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// rbac_api_context.go +package casbin + +import ( + "context" + + "github.com/casbin/casbin/v3/constant" + "github.com/casbin/casbin/v3/errors" + "github.com/casbin/casbin/v3/util" +) + +// AddRoleForUserCtx adds a role for a user with context support. +// Returns false if the user already has the role (aka not affected). +func (e *ContextEnforcer) AddRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error) { + args := []string{user, role} + args = append(args, domain...) + return e.AddGroupingPolicyCtx(ctx, args) +} + +// DeleteRoleForUserCtx deletes a role for a user with context support. +// Returns false if the user does not have the role (aka not affected). +func (e *ContextEnforcer) DeleteRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error) { + args := []string{user, role} + args = append(args, domain...) + return e.RemoveGroupingPolicyCtx(ctx, args) +} + +// DeleteRolesForUserCtx deletes all roles for a user with context support. +// Returns false if the user does not have any roles (aka not affected). +func (e *ContextEnforcer) DeleteRolesForUserCtx(ctx context.Context, user string, domain ...string) (bool, error) { + var args []string + if len(domain) == 0 { + args = []string{user} + } else if len(domain) > 1 { + return false, errors.ErrDomainParameter + } else { + args = []string{user, "", domain[0]} + } + return e.RemoveFilteredGroupingPolicyCtx(ctx, 0, args...) +} + +// DeleteUserCtx deletes a user with context support. +// Returns false if the user does not exist (aka not affected). +func (e *ContextEnforcer) DeleteUserCtx(ctx context.Context, user string) (bool, error) { + var err error + res1, err := e.RemoveFilteredGroupingPolicyCtx(ctx, 0, user) + if err != nil { + return res1, err + } + + subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex) + if err != nil { + return false, err + } + res2, err := e.RemoveFilteredPolicyCtx(ctx, subIndex, user) + return res1 || res2, err +} + +// DeleteRoleCtx deletes a role with context support. +// Returns false if the role does not exist (aka not affected). +func (e *ContextEnforcer) DeleteRoleCtx(ctx context.Context, role string) (bool, error) { + var err error + res1, err := e.RemoveFilteredGroupingPolicyCtx(ctx, 0, role) + if err != nil { + return res1, err + } + + res2, err := e.RemoveFilteredGroupingPolicyCtx(ctx, 1, role) + if err != nil { + return res1, err + } + + subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex) + if err != nil { + return false, err + } + res3, err := e.RemoveFilteredPolicyCtx(ctx, subIndex, role) + return res1 || res2 || res3, err +} + +// DeletePermissionCtx deletes a permission with context support. +// Returns false if the permission does not exist (aka not affected). +func (e *ContextEnforcer) DeletePermissionCtx(ctx context.Context, permission ...string) (bool, error) { + return e.RemoveFilteredPolicyCtx(ctx, 1, permission...) +} + +// AddPermissionForUserCtx adds a permission for a user or role with context support. +// Returns false if the user or role already has the permission (aka not affected). +func (e *ContextEnforcer) AddPermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error) { + return e.AddPolicyCtx(ctx, util.JoinSlice(user, permission...)) +} + +// AddPermissionsForUserCtx adds multiple permissions for a user or role with context support. +// Returns false if the user or role already has one of the permissions (aka not affected). +func (e *ContextEnforcer) AddPermissionsForUserCtx(ctx context.Context, user string, permissions ...[]string) (bool, error) { + var rules [][]string + for _, permission := range permissions { + rules = append(rules, util.JoinSlice(user, permission...)) + } + return e.AddPoliciesCtx(ctx, rules) +} + +// DeletePermissionForUserCtx deletes a permission for a user or role with context support. +// Returns false if the user or role does not have the permission (aka not affected). +func (e *ContextEnforcer) DeletePermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error) { + return e.RemovePolicyCtx(ctx, util.JoinSlice(user, permission...)) +} + +// DeletePermissionsForUserCtx deletes permissions for a user or role with context support. +// Returns false if the user or role does not have any permissions (aka not affected). +func (e *ContextEnforcer) DeletePermissionsForUserCtx(ctx context.Context, user string) (bool, error) { + subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex) + if err != nil { + return false, err + } + return e.RemoveFilteredPolicyCtx(ctx, subIndex, user) +} diff --git a/rbac_api_synced.go b/rbac_api_synced.go new file mode 100644 index 0000000..d7214f2 --- /dev/null +++ b/rbac_api_synced.go @@ -0,0 +1,221 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +// GetRolesForUser gets the roles that a user has. +func (e *SyncedEnforcer) GetRolesForUser(name string, domain ...string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetRolesForUser(name, domain...) +} + +// GetUsersForRole gets the users that has a role. +func (e *SyncedEnforcer) GetUsersForRole(name string, domain ...string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetUsersForRole(name, domain...) +} + +// HasRoleForUser determines whether a user has a role. +func (e *SyncedEnforcer) HasRoleForUser(name string, role string, domain ...string) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.HasRoleForUser(name, role, domain...) +} + +// AddRoleForUser adds a role for a user. +// Returns false if the user already has the role (aka not affected). +func (e *SyncedEnforcer) AddRoleForUser(user string, role string, domain ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddRoleForUser(user, role, domain...) +} + +// AddRolesForUser adds roles for a user. +// Returns false if the user already has the roles (aka not affected). +func (e *SyncedEnforcer) AddRolesForUser(user string, roles []string, domain ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddRolesForUser(user, roles, domain...) +} + +// DeleteRoleForUser deletes a role for a user. +// Returns false if the user does not have the role (aka not affected). +func (e *SyncedEnforcer) DeleteRoleForUser(user string, role string, domain ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeleteRoleForUser(user, role, domain...) +} + +// DeleteRolesForUser deletes all roles for a user. +// Returns false if the user does not have any roles (aka not affected). +func (e *SyncedEnforcer) DeleteRolesForUser(user string, domain ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeleteRolesForUser(user, domain...) +} + +// DeleteUser deletes a user. +// Returns false if the user does not exist (aka not affected). +func (e *SyncedEnforcer) DeleteUser(user string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeleteUser(user) +} + +// DeleteRole deletes a role. +// Returns false if the role does not exist (aka not affected). +func (e *SyncedEnforcer) DeleteRole(role string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeleteRole(role) +} + +// DeletePermission deletes a permission. +// Returns false if the permission does not exist (aka not affected). +func (e *SyncedEnforcer) DeletePermission(permission ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeletePermission(permission...) +} + +// AddPermissionForUser adds a permission for a user or role. +// Returns false if the user or role already has the permission (aka not affected). +func (e *SyncedEnforcer) AddPermissionForUser(user string, permission ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPermissionForUser(user, permission...) +} + +// AddPermissionsForUser adds permissions for a user or role. +// Returns false if the user or role already has the permissions (aka not affected). +func (e *SyncedEnforcer) AddPermissionsForUser(user string, permissions ...[]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPermissionsForUser(user, permissions...) +} + +// DeletePermissionForUser deletes a permission for a user or role. +// Returns false if the user or role does not have the permission (aka not affected). +func (e *SyncedEnforcer) DeletePermissionForUser(user string, permission ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeletePermissionForUser(user, permission...) +} + +// DeletePermissionsForUser deletes permissions for a user or role. +// Returns false if the user or role does not have any permissions (aka not affected). +func (e *SyncedEnforcer) DeletePermissionsForUser(user string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeletePermissionsForUser(user) +} + +// GetPermissionsForUser gets permissions for a user or role. +func (e *SyncedEnforcer) GetPermissionsForUser(user string, domain ...string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetPermissionsForUser(user, domain...) +} + +// GetNamedPermissionsForUser gets permissions for a user or role by named policy. +func (e *SyncedEnforcer) GetNamedPermissionsForUser(ptype string, user string, domain ...string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetNamedPermissionsForUser(ptype, user, domain...) +} + +// HasPermissionForUser determines whether a user has a permission. +func (e *SyncedEnforcer) HasPermissionForUser(user string, permission ...string) (bool, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.HasPermissionForUser(user, permission...) +} + +// GetImplicitRolesForUser gets implicit roles that a user has. +// Compared to GetRolesForUser(), this function retrieves indirect roles besides direct roles. +// For example: +// g, alice, role:admin +// g, role:admin, role:user +// +// GetRolesForUser("alice") can only get: ["role:admin"]. +// But GetImplicitRolesForUser("alice") will get: ["role:admin", "role:user"]. +func (e *SyncedEnforcer) GetImplicitRolesForUser(name string, domain ...string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetImplicitRolesForUser(name, domain...) +} + +// GetImplicitPermissionsForUser gets implicit permissions for a user or role. +// Compared to GetPermissionsForUser(), this function retrieves permissions for inherited roles. +// For example: +// p, admin, data1, read +// p, alice, data2, read +// g, alice, admin +// +// GetPermissionsForUser("alice") can only get: [["alice", "data2", "read"]]. +// But GetImplicitPermissionsForUser("alice") will get: [["admin", "data1", "read"], ["alice", "data2", "read"]]. +func (e *SyncedEnforcer) GetImplicitPermissionsForUser(user string, domain ...string) ([][]string, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.GetImplicitPermissionsForUser(user, domain...) +} + +// GetNamedImplicitPermissionsForUser gets implicit permissions for a user or role by named policy. +// Compared to GetNamedPermissionsForUser(), this function retrieves permissions for inherited roles. +// For example: +// p, admin, data1, read +// p2, admin, create +// g, alice, admin +// +// GetImplicitPermissionsForUser("alice") can only get: [["admin", "data1", "read"]], whose policy is default policy "p" +// But you can specify the named policy "p2" to get: [["admin", "create"]] by GetNamedImplicitPermissionsForUser("p2","alice"). +func (e *SyncedEnforcer) GetNamedImplicitPermissionsForUser(ptype string, gtype string, user string, domain ...string) ([][]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetNamedImplicitPermissionsForUser(ptype, gtype, user, domain...) +} + +// GetImplicitUsersForPermission gets implicit users for a permission. +// For example: +// p, admin, data1, read +// p, bob, data1, read +// g, alice, admin +// +// GetImplicitUsersForPermission("data1", "read") will get: ["alice", "bob"]. +// Note: only users will be returned, roles (2nd arg in "g") will be excluded. +func (e *SyncedEnforcer) GetImplicitUsersForPermission(permission ...string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetImplicitUsersForPermission(permission...) +} + +// GetImplicitObjectPatternsForUser returns all object patterns (with wildcards) that a user has for a given domain and action. +// For example: +// p, admin, chronicle/123, location/*, read +// p, user, chronicle/456, location/789, read +// g, alice, admin +// g, bob, user +// +// GetImplicitObjectPatternsForUser("alice", "chronicle/123", "read") will return ["location/*"]. +// GetImplicitObjectPatternsForUser("bob", "chronicle/456", "read") will return ["location/789"]. +func (e *SyncedEnforcer) GetImplicitObjectPatternsForUser(user string, domain string, action string) ([]string, error) { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetImplicitObjectPatternsForUser(user, domain, action) +} diff --git a/rbac_api_test.go b/rbac_api_test.go new file mode 100644 index 0000000..f1d3be4 --- /dev/null +++ b/rbac_api_test.go @@ -0,0 +1,894 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + "log" + "sort" + "testing" + + "github.com/casbin/casbin/v3/constant" + "github.com/casbin/casbin/v3/errors" + defaultrolemanager "github.com/casbin/casbin/v3/rbac/default-role-manager" + "github.com/casbin/casbin/v3/util" +) + +func testGetRoles(t *testing.T, e *Enforcer, res []string, name string, domain ...string) { + t.Helper() + myRes, err := e.GetRolesForUser(name, domain...) + if err != nil { + t.Error("Roles for ", name, " could not be fetched: ", err.Error()) + } + t.Log("Roles for ", name, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Roles for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func testGetUsers(t *testing.T, e *Enforcer, res []string, name string, domain ...string) { + t.Helper() + myRes, err := e.GetUsersForRole(name, domain...) + switch err { + case nil: + break + case errors.ErrNameNotFound: + t.Log("No name found") + default: + t.Error("Users for ", name, " could not be fetched: ", err.Error()) + } + t.Log("Users for ", name, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Users for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func testHasRole(t *testing.T, e *Enforcer, name string, role string, res bool, domain ...string) { + t.Helper() + myRes, err := e.HasRoleForUser(name, role, domain...) + if err != nil { + t.Error("HasRoleForUser returned an error: ", err.Error()) + } + t.Log(name, " has role ", role, ": ", myRes) + + if res != myRes { + t.Error(name, " has role ", role, ": ", myRes, ", supposed to be ", res) + } +} + +func TestRoleAPI(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + testGetRoles(t, e, []string{"data2_admin"}, "alice") + testGetRoles(t, e, []string{}, "bob") + testGetRoles(t, e, []string{}, "data2_admin") + testGetRoles(t, e, []string{}, "non_exist") + + testHasRole(t, e, "alice", "data1_admin", false) + testHasRole(t, e, "alice", "data2_admin", true) + + _, _ = e.AddRoleForUser("alice", "data1_admin") + + testGetRoles(t, e, []string{"data1_admin", "data2_admin"}, "alice") + testGetRoles(t, e, []string{}, "bob") + testGetRoles(t, e, []string{}, "data2_admin") + + _, _ = e.DeleteRoleForUser("alice", "data1_admin") + + testGetRoles(t, e, []string{"data2_admin"}, "alice") + testGetRoles(t, e, []string{}, "bob") + testGetRoles(t, e, []string{}, "data2_admin") + + _, _ = e.DeleteRolesForUser("alice") + + testGetRoles(t, e, []string{}, "alice") + testGetRoles(t, e, []string{}, "bob") + testGetRoles(t, e, []string{}, "data2_admin") + + _, _ = e.AddRoleForUser("alice", "data1_admin") + _, _ = e.DeleteUser("alice") + + testGetRoles(t, e, []string{}, "alice") + testGetRoles(t, e, []string{}, "bob") + testGetRoles(t, e, []string{}, "data2_admin") + + _, _ = e.AddRoleForUser("alice", "data2_admin") + + testEnforce(t, e, "alice", "data1", "read", false) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) + + _, _ = e.DeleteRole("data2_admin") + + testEnforce(t, e, "alice", "data1", "read", false) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + +func TestRoleAPI_Domains(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testHasRole(t, e, "alice", "admin", true, "domain1") + testHasRole(t, e, "alice", "admin", false, "domain2") + testGetRoles(t, e, []string{"admin"}, "alice", "domain1") + testGetRoles(t, e, []string{}, "bob", "domain1") + testGetRoles(t, e, []string{}, "admin", "domain1") + testGetRoles(t, e, []string{}, "non_exist", "domain1") + testGetRoles(t, e, []string{}, "alice", "domain2") + testGetRoles(t, e, []string{"admin"}, "bob", "domain2") + testGetRoles(t, e, []string{}, "admin", "domain2") + testGetRoles(t, e, []string{}, "non_exist", "domain2") + + _, _ = e.DeleteRoleForUser("alice", "admin", "domain1") + _, _ = e.AddRoleForUser("bob", "admin", "domain1") + + testGetRoles(t, e, []string{}, "alice", "domain1") + testGetRoles(t, e, []string{"admin"}, "bob", "domain1") + testGetRoles(t, e, []string{}, "admin", "domain1") + testGetRoles(t, e, []string{}, "non_exist", "domain1") + testGetRoles(t, e, []string{}, "alice", "domain2") + testGetRoles(t, e, []string{"admin"}, "bob", "domain2") + testGetRoles(t, e, []string{}, "admin", "domain2") + testGetRoles(t, e, []string{}, "non_exist", "domain2") + + _, _ = e.AddRoleForUser("alice", "admin", "domain1") + _, _ = e.DeleteRolesForUser("bob", "domain1") + + testGetRoles(t, e, []string{"admin"}, "alice", "domain1") + testGetRoles(t, e, []string{}, "bob", "domain1") + testGetRoles(t, e, []string{}, "admin", "domain1") + testGetRoles(t, e, []string{}, "non_exist", "domain1") + testGetRoles(t, e, []string{}, "alice", "domain2") + testGetRoles(t, e, []string{"admin"}, "bob", "domain2") + testGetRoles(t, e, []string{}, "admin", "domain2") + testGetRoles(t, e, []string{}, "non_exist", "domain2") + + _, _ = e.AddRolesForUser("bob", []string{"admin", "admin1", "admin2"}, "domain1") + + testGetRoles(t, e, []string{"admin", "admin1", "admin2"}, "bob", "domain1") + + testGetPermissions(t, e, "admin", [][]string{{"admin", "domain1", "data1", "read"}, {"admin", "domain1", "data1", "write"}}, "domain1") + testGetPermissions(t, e, "admin", [][]string{{"admin", "domain2", "data2", "read"}, {"admin", "domain2", "data2", "write"}}, "domain2") +} + +func TestEnforcer_AddRolesForUser(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + _, _ = e.AddRolesForUser("alice", []string{"data1_admin", "data2_admin", "data3_admin"}) + // The "alice" already has "data2_admin" , it will be return false. So "alice" just has "data2_admin". + testGetRoles(t, e, []string{"data2_admin"}, "alice") + // delete role + _, _ = e.DeleteRoleForUser("alice", "data2_admin") + + _, _ = e.AddRolesForUser("alice", []string{"data1_admin", "data2_admin", "data3_admin"}) + testGetRoles(t, e, []string{"data1_admin", "data2_admin", "data3_admin"}, "alice") + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) +} + +func testGetPermissions(t *testing.T, e *Enforcer, name string, res [][]string, domain ...string) { + t.Helper() + myRes, err := e.GetPermissionsForUser(name, domain...) + if err != nil { + t.Error(err.Error()) + } + t.Log("Permissions for ", name, ": ", myRes) + + if !util.Array2DEquals(res, myRes) { + t.Error("Permissions for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func testHasPermission(t *testing.T, e *Enforcer, name string, permission []string, res bool) { + t.Helper() + myRes, err := e.HasPermissionForUser(name, permission...) + if err != nil { + t.Error(err.Error()) + } + + t.Log(name, " has permission ", util.ArrayToString(permission), ": ", myRes) + + if res != myRes { + t.Error(name, " has permission ", util.ArrayToString(permission), ": ", myRes, ", supposed to be ", res) + } +} + +func testGetNamedPermissionsForUser(t *testing.T, e *Enforcer, ptype string, name string, res [][]string, domain ...string) { + t.Helper() + myRes, err := e.GetNamedPermissionsForUser(ptype, name, domain...) + if err != nil { + t.Error(err.Error()) + } + t.Log("Named permissions for ", name, ": ", myRes) + + if !util.Array2DEquals(res, myRes) { + t.Error("Named permissions for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func TestPermissionAPI(t *testing.T) { + e, _ := NewEnforcer("examples/basic_without_resources_model.conf", "examples/basic_without_resources_policy.csv") + + testEnforceWithoutUsers(t, e, "alice", "read", true) + testEnforceWithoutUsers(t, e, "alice", "write", false) + testEnforceWithoutUsers(t, e, "bob", "read", false) + testEnforceWithoutUsers(t, e, "bob", "write", true) + + testGetPermissions(t, e, "alice", [][]string{{"alice", "read"}}) + testGetPermissions(t, e, "bob", [][]string{{"bob", "write"}}) + + testHasPermission(t, e, "alice", []string{"read"}, true) + testHasPermission(t, e, "alice", []string{"write"}, false) + testHasPermission(t, e, "bob", []string{"read"}, false) + testHasPermission(t, e, "bob", []string{"write"}, true) + + _, _ = e.DeletePermission("read") + + testEnforceWithoutUsers(t, e, "alice", "read", false) + testEnforceWithoutUsers(t, e, "alice", "write", false) + testEnforceWithoutUsers(t, e, "bob", "read", false) + testEnforceWithoutUsers(t, e, "bob", "write", true) + + _, _ = e.AddPermissionForUser("bob", "read") + + testEnforceWithoutUsers(t, e, "alice", "read", false) + testEnforceWithoutUsers(t, e, "alice", "write", false) + testEnforceWithoutUsers(t, e, "bob", "read", true) + testEnforceWithoutUsers(t, e, "bob", "write", true) + + _, _ = e.AddPermissionsForUser("jack", + []string{"read"}, + []string{"write"}) + + testEnforceWithoutUsers(t, e, "jack", "read", true) + testEnforceWithoutUsers(t, e, "bob", "write", true) + + _, _ = e.DeletePermissionForUser("bob", "read") + + testEnforceWithoutUsers(t, e, "alice", "read", false) + testEnforceWithoutUsers(t, e, "alice", "write", false) + testEnforceWithoutUsers(t, e, "bob", "read", false) + testEnforceWithoutUsers(t, e, "bob", "write", true) + + _, _ = e.DeletePermissionsForUser("bob") + + testEnforceWithoutUsers(t, e, "alice", "read", false) + testEnforceWithoutUsers(t, e, "alice", "write", false) + testEnforceWithoutUsers(t, e, "bob", "read", false) + testEnforceWithoutUsers(t, e, "bob", "write", false) + + e, _ = NewEnforcer("examples/rbac_with_multiple_policy_model.conf", "examples/rbac_with_multiple_policy_policy.csv") + testGetNamedPermissionsForUser(t, e, "p", "user", [][]string{{"user", "/data", "GET"}}) + testGetNamedPermissionsForUser(t, e, "p2", "user", [][]string{{"user", "view"}}) +} + +func testGetImplicitRoles(t *testing.T, e *Enforcer, name string, res []string) { + t.Helper() + myRes, _ := e.GetImplicitRolesForUser(name) + t.Log("Implicit roles for ", name, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Implicit roles for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func testGetImplicitRolesInDomain(t *testing.T, e *Enforcer, name string, domain string, res []string) { + t.Helper() + myRes, _ := e.GetImplicitRolesForUser(name, domain) + t.Log("Implicit roles in domain ", domain, " for ", name, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Implicit roles in domain ", domain, " for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func TestImplicitRoleAPI(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_with_hierarchy_policy.csv") + + testGetPermissions(t, e, "alice", [][]string{{"alice", "data1", "read"}}) + testGetPermissions(t, e, "bob", [][]string{{"bob", "data2", "write"}}) + + testGetImplicitRoles(t, e, "alice", []string{"admin", "data1_admin", "data2_admin"}) + testGetImplicitRoles(t, e, "bob", []string{}) + + e, _ = NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv") + + e.GetRoleManager().AddMatchingFunc("matcher", util.KeyMatch) + e.AddNamedMatchingFunc("g2", "matcher", util.KeyMatch) + + // testGetImplicitRoles(t, e, "cathy", []string{"/book/1/2/3/4/5", "pen_admin", "/book/*", "book_group"}) + testGetImplicitRoles(t, e, "cathy", []string{"/book/1/2/3/4/5", "pen_admin"}) + testGetRoles(t, e, []string{"/book/1/2/3/4/5", "pen_admin"}, "cathy") +} + +func testGetImplicitPermissions(t *testing.T, e *Enforcer, name string, res [][]string, domain ...string) { + t.Helper() + myRes, _ := e.GetImplicitPermissionsForUser(name, domain...) + t.Log("Implicit permissions for ", name, ": ", myRes) + + if !util.Set2DEquals(res, myRes) { + t.Error("Implicit permissions for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func testGetImplicitPermissionsWithDomain(t *testing.T, e *Enforcer, name string, domain string, res [][]string) { + t.Helper() + myRes, _ := e.GetImplicitPermissionsForUser(name, domain) + t.Log("Implicit permissions for", name, "under", domain, ":", myRes) + + if !util.Set2DEquals(res, myRes) { + t.Error("Implicit permissions for", name, "under", domain, ":", myRes, ", supposed to be ", res) + } +} + +func testGetNamedImplicitPermissions(t *testing.T, e *Enforcer, ptype string, gtype string, name string, res [][]string) { + t.Helper() + myRes, _ := e.GetNamedImplicitPermissionsForUser(ptype, gtype, name) + t.Log("Named implicit permissions for ", name, ": ", myRes) + + if !util.Set2DEquals(res, myRes) { + t.Error("Named implicit permissions for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func TestImplicitPermissionAPI(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_with_hierarchy_policy.csv") + + testGetPermissions(t, e, "alice", [][]string{{"alice", "data1", "read"}}) + testGetPermissions(t, e, "bob", [][]string{{"bob", "data2", "write"}}) + + testGetImplicitPermissions(t, e, "alice", [][]string{{"alice", "data1", "read"}, {"data1_admin", "data1", "read"}, {"data1_admin", "data1", "write"}, {"data2_admin", "data2", "read"}, {"data2_admin", "data2", "write"}}) + testGetImplicitPermissions(t, e, "bob", [][]string{{"bob", "data2", "write"}}) + + e, _ = NewEnforcer("examples/rbac_with_domain_pattern_model.conf", "examples/rbac_with_domain_pattern_policy.csv") + e.AddNamedDomainMatchingFunc("g", "KeyMatch", util.KeyMatch) + + testGetImplicitPermissions(t, e, "admin", [][]string{{"admin", "domain1", "data1", "read"}, {"admin", "domain1", "data1", "write"}, {"admin", "domain1", "data3", "read"}}, "domain1") + + _, err := e.GetImplicitPermissionsForUser("admin", "domain1", "domain2") + if err == nil { + t.Error("GetImplicitPermissionsForUser should not support multiple domains") + } + + testGetImplicitPermissions(t, e, "alice", + [][]string{{"admin", "domain2", "data2", "read"}, {"admin", "domain2", "data2", "write"}, {"admin", "domain2", "data3", "read"}}, + "domain2") + + e, _ = NewEnforcer("examples/rbac_with_multiple_policy_model.conf", "examples/rbac_with_multiple_policy_policy.csv") + + testGetNamedImplicitPermissions(t, e, "p", "g", "alice", [][]string{{"user", "/data", "GET"}, {"admin", "/data", "POST"}}) + testGetNamedImplicitPermissions(t, e, "p2", "g", "alice", [][]string{{"user", "view"}, {"admin", "create"}}) + + testGetNamedImplicitPermissions(t, e, "p", "g2", "alice", [][]string{{"user", "/data", "GET"}}) + testGetNamedImplicitPermissions(t, e, "p2", "g2", "alice", [][]string{{"user", "view"}}) +} + +func TestImplicitPermissionAPIWithDomain(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_hierarchy_with_domains_policy.csv") + testGetImplicitPermissionsWithDomain(t, e, "alice", "domain1", [][]string{{"alice", "domain1", "data2", "read"}, {"role:reader", "domain1", "data1", "read"}, {"role:writer", "domain1", "data1", "write"}}) +} + +func testGetImplicitUsers(t *testing.T, e *Enforcer, res []string, permission ...string) { + t.Helper() + myRes, _ := e.GetImplicitUsersForPermission(permission...) + t.Log("Implicit users for permission: ", permission, ": ", myRes) + + sort.Strings(res) + sort.Strings(myRes) + + if !util.ArrayEquals(res, myRes) { + t.Error("Implicit users for permission: ", permission, ": ", myRes, ", supposed to be ", res) + } +} + +func TestImplicitUserAPI(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_with_hierarchy_policy.csv") + + testGetImplicitUsers(t, e, []string{"alice"}, "data1", "read") + testGetImplicitUsers(t, e, []string{"alice"}, "data1", "write") + testGetImplicitUsers(t, e, []string{"alice"}, "data2", "read") + testGetImplicitUsers(t, e, []string{"alice", "bob"}, "data2", "write") + + e.ClearPolicy() + _, _ = e.AddPolicy("admin", "data1", "read") + _, _ = e.AddPolicy("bob", "data1", "read") + _, _ = e.AddGroupingPolicy("alice", "admin") + testGetImplicitUsers(t, e, []string{"alice", "bob"}, "data1", "read") +} + +func testGetImplicitResourcesForUser(t *testing.T, e *Enforcer, res [][]string, user string, domain ...string) { + t.Helper() + myRes, _ := e.GetImplicitResourcesForUser(user, domain...) + t.Log("Implicit resources for user: ", user, ": ", myRes) + + lessFunc := func(arr [][]string) func(int, int) bool { + return func(i, j int) bool { + policy1, policy2 := arr[i], arr[j] + for k := range policy1 { + if policy1[k] == policy2[k] { + continue + } + return policy1[k] < policy2[k] + } + return true + } + } + + sort.Slice(res, lessFunc(res)) + sort.Slice(myRes, lessFunc(myRes)) + + if !util.Array2DEquals(res, myRes) { + t.Error("Implicit resources for user: ", user, ": ", myRes, ", supposed to be ", res) + } +} + +func TestGetImplicitResourcesForUser(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv") + testGetImplicitResourcesForUser(t, e, [][]string{ + {"alice", "/pen/1", "GET"}, + {"alice", "/pen2/1", "GET"}, + {"alice", "/book/:id", "GET"}, + {"alice", "/book2/{id}", "GET"}, + {"alice", "/book/*", "GET"}, + {"alice", "book_group", "GET"}, + }, "alice") + testGetImplicitResourcesForUser(t, e, [][]string{ + {"bob", "pen_group", "GET"}, + {"bob", "/pen/:id", "GET"}, + {"bob", "/pen2/{id}", "GET"}, + }, "bob") + testGetImplicitResourcesForUser(t, e, [][]string{ + {"cathy", "pen_group", "GET"}, + {"cathy", "/pen/:id", "GET"}, + {"cathy", "/pen2/{id}", "GET"}, + }, "cathy") +} + +func TestImplicitUsersForRole(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv") + + testGetImplicitUsersForRole(t, e, "book_admin", []string{"alice"}) + testGetImplicitUsersForRole(t, e, "pen_admin", []string{"cathy", "bob"}) + + testGetImplicitUsersForRole(t, e, "book_group", []string{"/book/*", "/book/:id", "/book2/{id}"}) + testGetImplicitUsersForRole(t, e, "pen_group", []string{"/pen/:id", "/pen2/{id}"}) +} + +func testGetImplicitUsersForRole(t *testing.T, e *Enforcer, name string, res []string) { + t.Helper() + myRes, _ := e.GetImplicitUsersForRole(name) + t.Log("Implicit users for ", name, ": ", myRes) + sort.Strings(res) + sort.Strings(myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Implicit users for ", name, ": ", myRes, ", supposed to be ", res) + } +} + +func TestExplicitPriorityModify(t *testing.T) { + e, _ := NewEnforcer("examples/priority_model_explicit.conf", "examples/priority_policy_explicit.csv") + + testEnforce(t, e, "bob", "data2", "write", true) + _, err := e.AddPolicy("1", "bob", "data2", "write", "deny") + if err != nil { + t.Fatalf("AddPolicy: %v", err) + } + testEnforce(t, e, "bob", "data2", "write", false) + + _, err = e.DeletePermissionsForUser("bob") + if err != nil { + t.Fatalf("DeletePermissionForUser: %v", err) + } + testEnforce(t, e, "bob", "data2", "write", true) + + _, err = e.DeleteRole("data2_allow_group") + if err != nil { + t.Fatalf("DeleteRole: %v", err) + } + testEnforce(t, e, "bob", "data2", "write", false) +} + +func TestCustomizedFieldIndex(t *testing.T) { + e, _ := NewEnforcer("examples/priority_model_explicit_customized.conf", + "examples/priority_policy_explicit_customized.csv") + + // Due to the customized priority token, the enforcer failed to handle the priority. + testEnforce(t, e, "bob", "data2", "read", true) + + // set PriorityIndex and reload + e.SetFieldIndex("p", constant.PriorityIndex, 0) + err := e.LoadPolicy() + if err != nil { + t.Fatalf("LoadPolicy: %v", err) + } + testEnforce(t, e, "bob", "data2", "read", false) + + testEnforce(t, e, "bob", "data2", "write", true) + _, err = e.AddPolicy("1", "data2", "write", "deny", "bob") + if err != nil { + t.Fatalf("AddPolicy: %v", err) + } + testEnforce(t, e, "bob", "data2", "write", false) + + // Due to the customized subject token, the enforcer will raise an error before SetFieldIndex. + _, err = e.DeletePermissionsForUser("bob") + if err == nil { + t.Fatalf("Failed to warning SetFieldIndex") + } + + e.SetFieldIndex("p", constant.SubjectIndex, 4) + + _, err = e.DeletePermissionsForUser("bob") + if err != nil { + t.Fatalf("DeletePermissionForUser: %v", err) + } + testEnforce(t, e, "bob", "data2", "write", true) + + _, err = e.DeleteRole("data2_allow_group") + if err != nil { + t.Fatalf("DeleteRole: %v", err) + } + testEnforce(t, e, "bob", "data2", "write", false) +} + +func testGetAllowedObjectConditions(t *testing.T, e *Enforcer, user string, act string, prefix string, res []string, expectedErr error) { + myRes, actualErr := e.GetAllowedObjectConditions(user, act, prefix) + + if actualErr != expectedErr { + t.Error("actual Err: ", actualErr, ", supposed to be ", expectedErr) + } + if actualErr == nil { + log.Print("Policy: ", myRes) + if !util.ArrayEquals(res, myRes) { + t.Error("Policy: ", myRes, ", supposed to be ", res) + } + } +} + +func TestGetAllowedObjectConditions(t *testing.T) { + e, _ := NewEnforcer("examples/object_conditions_model.conf", "examples/object_conditions_policy.csv") + testGetAllowedObjectConditions(t, e, "alice", "read", "r.obj.", []string{"price < 25", "category_id = 2"}, nil) + testGetAllowedObjectConditions(t, e, "admin", "read", "r.obj.", []string{"category_id = 2"}, nil) + testGetAllowedObjectConditions(t, e, "bob", "write", "r.obj.", []string{"author = bob"}, nil) + + // test ErrEmptyCondition + testGetAllowedObjectConditions(t, e, "alice", "write", "r.obj.", []string{}, errors.ErrEmptyCondition) + testGetAllowedObjectConditions(t, e, "bob", "read", "r.obj.", []string{}, errors.ErrEmptyCondition) + + // test ErrObjCondition + // should : e.AddPolicy("alice", "r.obj.price > 50", "read") + ok, _ := e.AddPolicy("alice", "price > 50", "read") + if ok { + testGetAllowedObjectConditions(t, e, "alice", "read", "r.obj.", []string{}, errors.ErrObjCondition) + } + + // test prefix + e.ClearPolicy() + err := e.GetRoleManager().DeleteLink("alice", "admin") + if err != nil { + panic(err) + } + ok, _ = e.AddPolicies([][]string{ + {"alice", "r.book.price < 25", "read"}, + {"admin", "r.book.category_id = 2", "read"}, + {"bob", "r.book.author = bob", "write"}, + }) + if ok { + testGetAllowedObjectConditions(t, e, "alice", "read", "r.book.", []string{"price < 25"}, nil) + testGetAllowedObjectConditions(t, e, "admin", "read", "r.book.", []string{"category_id = 2"}, nil) + testGetAllowedObjectConditions(t, e, "bob", "write", "r.book.", []string{"author = bob"}, nil) + } +} + +func testGetImplicitUsersForResource(t *testing.T, e *Enforcer, res [][]string, resource string, domain ...string) { + t.Helper() + myRes, err := e.GetImplicitUsersForResource(resource) + if err != nil { + panic(err) + } + + if !util.Set2DEquals(res, myRes) { + t.Error("Implicit users for ", resource, "in domain ", domain, " : ", myRes, ", supposed to be ", res) + } else { + t.Log("Implicit users for ", resource, "in domain ", domain, " : ", myRes) + } +} + +func TestGetImplicitUsersForResource(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + testGetImplicitUsersForResource(t, e, [][]string{{"alice", "data1", "read"}}, "data1") + testGetImplicitUsersForResource(t, e, [][]string{ + {"bob", "data2", "write"}, + {"alice", "data2", "read"}, + {"alice", "data2", "write"}, + }, "data2") + + // test duplicate permissions + _, _ = e.AddGroupingPolicy("alice", "data2_admin_2") + _, _ = e.AddPolicies([][]string{{"data2_admin_2", "data2", "read"}, {"data2_admin_2", "data2", "write"}}) + testGetImplicitUsersForResource(t, e, [][]string{ + {"bob", "data2", "write"}, + {"alice", "data2", "read"}, + {"alice", "data2", "write"}, + }, "data2") +} + +func TestGetImplicitUsersForResourceWithResourceRoles(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_resource_roles_model.conf", "examples/rbac_with_resource_roles_policy.csv") + + // Test data1 resource - should return users who have access through g2 relationships + data1Users, err := e.GetNamedImplicitUsersForResource("g2", "data1") + if err != nil { + t.Fatalf("GetNamedImplicitUsersForResource failed: %v", err) + } + + expectedData1Users := 2 // [alice data1 read] + [alice data_group write] + if len(data1Users) != expectedData1Users { + t.Errorf("Expected %d users for data1 resource, got %d: %v", expectedData1Users, len(data1Users), data1Users) + } + + // Test data2 resource - should return users who have access through g2 relationships + data2Users, err := e.GetNamedImplicitUsersForResource("g2", "data2") + if err != nil { + t.Fatalf("GetNamedImplicitUsersForResource failed: %v", err) + } + + expectedData2Users := 2 // [bob data2 write] + [alice data_group write] + if len(data2Users) != expectedData2Users { + t.Errorf("Expected %d users for data2 resource, got %d: %v", expectedData2Users, len(data2Users), data2Users) + } + + // Test with "g" policy type - should return users who have access through g relationships + data1UsersG, err := e.GetNamedImplicitUsersForResource("g", "data1") + if err != nil { + t.Fatalf("GetNamedImplicitUsersForResource with g failed: %v", err) + } + + expectedData1UsersG := 1 // [alice data1 read] only + if len(data1UsersG) != expectedData1UsersG { + t.Errorf("Expected %d users for data1 resource with g policy, got %d: %v", expectedData1UsersG, len(data1UsersG), data1UsersG) + } +} + +func testGetImplicitUsersForResourceByDomain(t *testing.T, e *Enforcer, res [][]string, resource string, domain string) { + t.Helper() + myRes, err := e.GetImplicitUsersForResourceByDomain(resource, domain) + if err != nil { + panic(err) + } + + if !util.Set2DEquals(res, myRes) { + t.Error("Implicit users for ", resource, "in domain ", domain, " : ", myRes, ", supposed to be ", res) + } else { + t.Log("Implicit users for ", resource, "in domain ", domain, " : ", myRes) + } +} + +func TestGetImplicitUsersForResourceByDomain(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + testGetImplicitUsersForResourceByDomain(t, e, [][]string{ + {"alice", "domain1", "data1", "read"}, + {"alice", "domain1", "data1", "write"}, + }, "data1", "domain1") + + testGetImplicitUsersForResourceByDomain(t, e, [][]string{}, "data2", "domain1") + + testGetImplicitUsersForResourceByDomain(t, e, [][]string{ + {"bob", "domain2", "data2", "read"}, + {"bob", "domain2", "data2", "write"}, + }, "data2", "domain2") +} + +func TestConditional(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_conditional_model.conf", "examples/rbac_with_domains_conditional_policy.csv") + g, _ := e.GetNamedGroupingPolicy("g") + for _, gp := range g { + e.AddNamedDomainLinkConditionFunc("g", gp[0], gp[1], gp[2], util.TimeMatchFunc) + } + + testDomainEnforce(t, e, "alice", "domain1", "service1", "/list", true) + testDomainEnforce(t, e, "bob", "domain2", "service2", "/broadcast", true) + testDomainEnforce(t, e, "jack", "domain1", "service1", "/list", false) + testGetImplicitRolesInDomain(t, e, "alice", "domain1", []string{"test1"}) + testGetRolesInDomain(t, e, "alice", "domain1", []string{"test1"}) + testGetUsersInDomain(t, e, "test1", "domain1", []string{"alice"}) +} + +func TestMaxHierarchyLevelConsistency(t *testing.T) { + // Test consistency behavior under different maxHierarchyLevel values + testCases := []struct { + maxLevel int + name string + }{ + {1, "maxHierarchyLevel=1"}, + {2, "maxHierarchyLevel=2"}, + {3, "maxHierarchyLevel=3"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Use model files from examples + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Set the maximum hierarchy level for role manager + rm := defaultrolemanager.NewRoleManager(tc.maxLevel) + e.SetRoleManager(rm) + + // Add role hierarchy: level0 -> level1 -> level2 -> level3 -> level4 + _, err = e.AddRoleForUser("level0", "level1") + if err != nil { + t.Fatalf("Failed to add role for user: %v", err) + } + _, err = e.AddRoleForUser("level1", "level2") + if err != nil { + t.Fatalf("Failed to add role for user: %v", err) + } + _, err = e.AddRoleForUser("level2", "level3") + if err != nil { + t.Fatalf("Failed to add role for user: %v", err) + } + _, err = e.AddRoleForUser("level3", "level4") + if err != nil { + t.Fatalf("Failed to add role for user: %v", err) + } + + // Test HasLink method + t.Run("HasLink", func(t *testing.T) { + for i := 1; i <= 4; i++ { + hasLink, err := rm.HasLink("level0", fmt.Sprintf("level%d", i)) + if err != nil { + t.Fatalf("HasLink error: %v", err) + } + expected := i <= tc.maxLevel + if hasLink != expected { + t.Errorf("HasLink(level0, level%d): got %v, want %v", i, hasLink, expected) + } + } + }) + + // Test GetImplicitRolesForUser method + t.Run("GetImplicitRolesForUser", func(t *testing.T) { + implicitRoles, err := e.GetImplicitRolesForUser("level0") + if err != nil { + t.Fatalf("GetImplicitRolesForUser error: %v", err) + } + + expectedCount := tc.maxLevel + if len(implicitRoles) != expectedCount { + t.Errorf("GetImplicitRolesForUser(level0): got %d roles %v, want %d roles", + len(implicitRoles), implicitRoles, expectedCount) + } + + // Verify that returned roles are correct + for i := 1; i <= tc.maxLevel; i++ { + expectedRole := fmt.Sprintf("level%d", i) + found := false + for _, role := range implicitRoles { + if role == expectedRole { + found = true + break + } + } + if !found { + t.Errorf("Expected role %s not found in implicit roles: %v", expectedRole, implicitRoles) + } + } + }) + + // Test GetImplicitUsersForRole method + t.Run("GetImplicitUsersForRole", func(t *testing.T) { + implicitUsers, err := e.GetImplicitUsersForRole("level4") + if err != nil { + t.Fatalf("GetImplicitUsersForRole error: %v", err) + } + + expectedCount := tc.maxLevel + if len(implicitUsers) != expectedCount { + t.Errorf("GetImplicitUsersForRole(level4): got %d users %v, want %d users", + len(implicitUsers), implicitUsers, expectedCount) + } + + // Verify that returned users are correct (starting from level3 upward) + for i := 0; i < tc.maxLevel; i++ { + expectedUser := fmt.Sprintf("level%d", 3-i) + found := false + for _, user := range implicitUsers { + if user == expectedUser { + found = true + break + } + } + if !found { + t.Errorf("Expected user %s not found in implicit users: %v", expectedUser, implicitUsers) + } + } + }) + + // Test implicit roles for different users + t.Run("DifferentUsersImplicitRoles", func(t *testing.T) { + for i := 0; i <= 3; i++ { + user := fmt.Sprintf("level%d", i) + implicitRoles, err := e.GetImplicitRolesForUser(user) + if err != nil { + t.Fatalf("GetImplicitRolesForUser(%s) error: %v", user, err) + } + + // Verify that the number of returned roles does not exceed maxHierarchyLevel + if len(implicitRoles) > tc.maxLevel { + t.Errorf("GetImplicitRolesForUser(%s): got %d roles, should not exceed maxHierarchyLevel %d", + user, len(implicitRoles), tc.maxLevel) + } + } + }) + }) + } +} + +func testGetImplicitObjectPatternsForUser(t *testing.T, e *Enforcer, user string, domain string, action string, res []string) { + t.Helper() + myRes, err := e.GetImplicitObjectPatternsForUser(user, domain, action) + if err != nil { + t.Error("Implicit object patterns for ", user, " under domain ", domain, " with action ", action, " could not be fetched: ", err.Error()) + } + t.Log("Implicit object patterns for ", user, " under domain ", domain, " with action ", action, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Implicit object patterns for ", user, " under domain ", domain, " with action ", action, ": ", myRes, ", supposed to be ", res) + } +} + +func TestGetImplicitObjectPatternsForUser(t *testing.T) { + // Test with domain pattern model + e, _ := NewEnforcer("examples/rbac_with_domain_pattern_model.conf", "examples/rbac_with_domain_pattern_policy.csv") + e.AddNamedDomainMatchingFunc("g", "KeyMatch", util.KeyMatch) + + // Test case 1: admin user with wildcard domain access + testGetImplicitObjectPatternsForUser(t, e, "admin", "domain1", "read", []string{"data1", "data3"}) + testGetImplicitObjectPatternsForUser(t, e, "admin", "domain1", "write", []string{"data1"}) + + // Test case 2: alice user inheriting admin role in domain2 + testGetImplicitObjectPatternsForUser(t, e, "alice", "domain2", "read", []string{"data2", "data3"}) + testGetImplicitObjectPatternsForUser(t, e, "alice", "domain2", "write", []string{"data2"}) + + // Test case 3: bob user with specific domain access + testGetImplicitObjectPatternsForUser(t, e, "bob", "domain2", "read", []string{"data2", "data3"}) + testGetImplicitObjectPatternsForUser(t, e, "bob", "domain2", "write", []string{"data2"}) + + // Test case 4: non-existent domain (admin has wildcard access to data3) + testGetImplicitObjectPatternsForUser(t, e, "admin", "non_existent", "read", []string{"data3"}) + + // Test case 5: non-existent action + testGetImplicitObjectPatternsForUser(t, e, "admin", "domain1", "non_existent", []string{}) +} diff --git a/rbac_api_with_domains.go b/rbac_api_with_domains.go new file mode 100644 index 0000000..9680471 --- /dev/null +++ b/rbac_api_with_domains.go @@ -0,0 +1,204 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + + "github.com/casbin/casbin/v3/constant" +) + +// GetUsersForRoleInDomain gets the users that has a role inside a domain. Add by Gordon. +func (e *Enforcer) GetUsersForRoleInDomain(name string, domain string) []string { + if e.GetRoleManager() == nil { + return nil + } + res, _ := e.GetRoleManager().GetUsers(name, domain) + return res +} + +// GetRolesForUserInDomain gets the roles that a user has inside a domain. +func (e *Enforcer) GetRolesForUserInDomain(name string, domain string) []string { + if rm := e.GetRoleManager(); rm != nil { + res, _ := rm.GetRoles(name, domain) + return res + } + return nil +} + +// GetPermissionsForUserInDomain gets permissions for a user or role inside a domain. +func (e *Enforcer) GetPermissionsForUserInDomain(user string, domain string) [][]string { + res, _ := e.GetImplicitPermissionsForUser(user, domain) + return res +} + +// AddRoleForUserInDomain adds a role for a user inside a domain. +// Returns false if the user already has the role (aka not affected). +func (e *Enforcer) AddRoleForUserInDomain(user string, role string, domain string) (bool, error) { + return e.AddGroupingPolicy(user, role, domain) +} + +// DeleteRoleForUserInDomain deletes a role for a user inside a domain. +// Returns false if the user does not have the role (aka not affected). +func (e *Enforcer) DeleteRoleForUserInDomain(user string, role string, domain string) (bool, error) { + return e.RemoveGroupingPolicy(user, role, domain) +} + +// DeleteRolesForUserInDomain deletes all roles for a user inside a domain. +// Returns false if the user does not have any roles (aka not affected). +func (e *Enforcer) DeleteRolesForUserInDomain(user string, domain string) (bool, error) { + if e.GetRoleManager() == nil { + return false, fmt.Errorf("role manager is not initialized") + } + roles, err := e.GetRoleManager().GetRoles(user, domain) + if err != nil { + return false, err + } + + var rules [][]string + for _, role := range roles { + rules = append(rules, []string{user, role, domain}) + } + + return e.RemoveGroupingPolicies(rules) +} + +// GetAllUsersByDomain would get all users associated with the domain. +func (e *Enforcer) GetAllUsersByDomain(domain string) ([]string, error) { + m := make(map[string]struct{}) + g, err := e.model.GetAssertion("g", "g") + if err != nil { + return []string{}, err + } + p := e.model["p"]["p"] + users := make([]string, 0) + index, err := e.GetFieldIndex("p", constant.DomainIndex) + if err != nil { + return []string{}, err + } + + getUser := func(index int, policies [][]string, domain string, m map[string]struct{}) []string { + if len(policies) == 0 || len(policies[0]) <= index { + return []string{} + } + res := make([]string, 0) + for _, policy := range policies { + if _, ok := m[policy[0]]; policy[index] == domain && !ok { + res = append(res, policy[0]) + m[policy[0]] = struct{}{} + } + } + return res + } + + users = append(users, getUser(2, g.Policy, domain, m)...) + users = append(users, getUser(index, p.Policy, domain, m)...) + return users, nil +} + +// DeleteAllUsersByDomain would delete all users associated with the domain. +func (e *Enforcer) DeleteAllUsersByDomain(domain string) (bool, error) { + g, err := e.model.GetAssertion("g", "g") + if err != nil { + return false, err + } + p := e.model["p"]["p"] + index, err := e.GetFieldIndex("p", constant.DomainIndex) + if err != nil { + return false, err + } + + getUser := func(index int, policies [][]string, domain string) [][]string { + if len(policies) == 0 || len(policies[0]) <= index { + return [][]string{} + } + res := make([][]string, 0) + for _, policy := range policies { + if policy[index] == domain { + res = append(res, policy) + } + } + return res + } + + users := getUser(2, g.Policy, domain) + if _, err = e.RemoveGroupingPolicies(users); err != nil { + return false, err + } + users = getUser(index, p.Policy, domain) + if _, err = e.RemovePolicies(users); err != nil { + return false, err + } + return true, nil +} + +// DeleteDomains would delete all associated policies. +// It would delete all domains if parameter is not provided. +func (e *Enforcer) DeleteDomains(domains ...string) (bool, error) { + if len(domains) == 0 { + var err error + domains, err = e.GetAllDomains() + if err != nil { + return false, err + } + } + for _, domain := range domains { + if _, err := e.DeleteAllUsersByDomain(domain); err != nil { + return false, err + } + // remove the domain from the RoleManager. + if e.GetRoleManager() != nil { + if err := e.GetRoleManager().DeleteDomain(domain); err != nil { + return false, err + } + } + } + return true, nil +} + +// GetAllDomains would get all domains. +func (e *Enforcer) GetAllDomains() ([]string, error) { + if e.GetRoleManager() == nil { + return nil, fmt.Errorf("role manager is not initialized") + } + return e.GetRoleManager().GetAllDomains() +} + +// GetAllRolesByDomain would get all roles associated with the domain. +// note: Not applicable to Domains with inheritance relationship (implicit roles) +func (e *Enforcer) GetAllRolesByDomain(domain string) ([]string, error) { + g, err := e.model.GetAssertion("g", "g") + if err != nil { + return []string{}, err + } + policies := g.Policy + roles := make([]string, 0) + existMap := make(map[string]bool) // remove duplicates + + for _, policy := range policies { + if policy[len(policy)-1] == domain { + role := policy[len(policy)-2] + if _, ok := existMap[role]; !ok { + roles = append(roles, role) + existMap[role] = true + } + } + } + + return roles, nil +} diff --git a/rbac_api_with_domains_context.go b/rbac_api_with_domains_context.go new file mode 100644 index 0000000..faab430 --- /dev/null +++ b/rbac_api_with_domains_context.go @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "context" + "fmt" + + "github.com/casbin/casbin/v3/constant" +) + +// AddRoleForUserInDomainCtx adds a role for a user inside a domain with context support. +// Returns false if the user already has the role (aka not affected). +func (e *ContextEnforcer) AddRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error) { + return e.AddGroupingPolicyCtx(ctx, user, role, domain) +} + +// DeleteRoleForUserInDomainCtx deletes a role for a user inside a domain with context support. +// Returns false if the user does not have the role (aka not affected). +func (e *ContextEnforcer) DeleteRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error) { + return e.RemoveGroupingPolicyCtx(ctx, user, role, domain) +} + +// DeleteRolesForUserInDomainCtx deletes all roles for a user inside a domain with context support. +// Returns false if the user does not have any roles (aka not affected). +func (e *ContextEnforcer) DeleteRolesForUserInDomainCtx(ctx context.Context, user string, domain string) (bool, error) { + if e.GetRoleManager() == nil { + return false, fmt.Errorf("role manager is not initialized") + } + roles, err := e.GetRoleManager().GetRoles(user, domain) + if err != nil { + return false, err + } + + var rules [][]string + for _, role := range roles { + rules = append(rules, []string{user, role, domain}) + } + + return e.RemoveGroupingPoliciesCtx(ctx, rules) +} + +// DeleteAllUsersByDomainCtx deletes all users associated with the domain with context support. +func (e *ContextEnforcer) DeleteAllUsersByDomainCtx(ctx context.Context, domain string) (bool, error) { + g, err := e.model.GetAssertion("g", "g") + if err != nil { + return false, err + } + p := e.model["p"]["p"] + index, err := e.GetFieldIndex("p", constant.DomainIndex) + if err != nil { + return false, err + } + + getUser := func(index int, policies [][]string, domain string) [][]string { + if len(policies) == 0 || len(policies[0]) <= index { + return [][]string{} + } + res := make([][]string, 0) + for _, policy := range policies { + if policy[index] == domain { + res = append(res, policy) + } + } + return res + } + + users := getUser(2, g.Policy, domain) + if _, err = e.RemoveGroupingPoliciesCtx(ctx, users); err != nil { + return false, err + } + users = getUser(index, p.Policy, domain) + if _, err = e.RemovePoliciesCtx(ctx, users); err != nil { + return false, err + } + return true, nil +} + +// DeleteDomainsCtx deletes all associated policies for domains with context support. +// It would delete all domains if parameter is not provided. +func (e *ContextEnforcer) DeleteDomainsCtx(ctx context.Context, domains ...string) (bool, error) { + if len(domains) == 0 { + var err error + domains, err = e.GetAllDomains() + if err != nil { + return false, err + } + } + for _, domain := range domains { + if _, err := e.DeleteAllUsersByDomainCtx(ctx, domain); err != nil { + return false, err + } + // remove the domain from the RoleManager. + if e.GetRoleManager() != nil { + if err := e.GetRoleManager().DeleteDomain(domain); err != nil { + return false, err + } + } + } + return true, nil +} diff --git a/rbac_api_with_domains_synced.go b/rbac_api_with_domains_synced.go new file mode 100644 index 0000000..e1402af --- /dev/null +++ b/rbac_api_with_domains_synced.go @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +// GetUsersForRoleInDomain gets the users that has a role inside a domain. Add by Gordon. +func (e *SyncedEnforcer) GetUsersForRoleInDomain(name string, domain string) []string { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetUsersForRoleInDomain(name, domain) +} + +// GetRolesForUserInDomain gets the roles that a user has inside a domain. +func (e *SyncedEnforcer) GetRolesForUserInDomain(name string, domain string) []string { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetRolesForUserInDomain(name, domain) +} + +// GetPermissionsForUserInDomain gets permissions for a user or role inside a domain. +func (e *SyncedEnforcer) GetPermissionsForUserInDomain(user string, domain string) [][]string { + e.m.RLock() + defer e.m.RUnlock() + return e.Enforcer.GetPermissionsForUserInDomain(user, domain) +} + +// AddRoleForUserInDomain adds a role for a user inside a domain. +// Returns false if the user already has the role (aka not affected). +func (e *SyncedEnforcer) AddRoleForUserInDomain(user string, role string, domain string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddRoleForUserInDomain(user, role, domain) +} + +// DeleteRoleForUserInDomain deletes a role for a user inside a domain. +// Returns false if the user does not have the role (aka not affected). +func (e *SyncedEnforcer) DeleteRoleForUserInDomain(user string, role string, domain string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeleteRoleForUserInDomain(user, role, domain) +} + +// DeleteRolesForUserInDomain deletes all roles for a user inside a domain. +// Returns false if the user does not have any roles (aka not affected). +func (e *SyncedEnforcer) DeleteRolesForUserInDomain(user string, domain string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeleteRolesForUserInDomain(user, domain) +} + +// DeleteDomains deletes domains from the model. +// Returns false if the domain does not exist (aka not affected). +func (e *SyncedEnforcer) DeleteDomains(domains ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.DeleteDomains(domains...) +} diff --git a/rbac_api_with_domains_test.go b/rbac_api_with_domains_test.go new file mode 100644 index 0000000..1437b28 --- /dev/null +++ b/rbac_api_with_domains_test.go @@ -0,0 +1,428 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "sort" + "testing" + + "github.com/casbin/casbin/v3/util" +) + +// testGetUsersInDomain: Add by Gordon. +func testGetUsersInDomain(t *testing.T, e *Enforcer, name string, domain string, res []string) { + t.Helper() + myRes := e.GetUsersForRoleInDomain(name, domain) + t.Log("Users for ", name, " under ", domain, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Users for ", name, " under ", domain, ": ", myRes, ", supposed to be ", res) + } +} + +func testGetRolesInDomain(t *testing.T, e *Enforcer, name string, domain string, res []string) { + t.Helper() + myRes := e.GetRolesForUserInDomain(name, domain) + t.Log("Roles for ", name, " under ", domain, ": ", myRes) + + if !util.SetEquals(res, myRes) { + t.Error("Roles for ", name, " under ", domain, ": ", myRes, ", supposed to be ", res) + } +} + +func TestGetImplicitRolesForDomainUser(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_hierarchy_with_domains_policy.csv") + + // This is only able to retrieve the first level of roles. + testGetRolesInDomain(t, e, "alice", "domain1", []string{"role:global_admin"}) + + // Retrieve all inherit roles. It supports domains as well. + testGetImplicitRolesInDomain(t, e, "alice", "domain1", []string{"role:global_admin", "role:reader", "role:writer"}) +} + +// TestUserAPIWithDomains: Add by Gordon. +func TestUserAPIWithDomains(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testGetUsers(t, e, []string{"alice"}, "admin", "domain1") + testGetUsersInDomain(t, e, "admin", "domain1", []string{"alice"}) + + testGetUsers(t, e, []string{}, "non_exist", "domain1") + testGetUsersInDomain(t, e, "non_exist", "domain1", []string{}) + + testGetUsers(t, e, []string{"bob"}, "admin", "domain2") + testGetUsersInDomain(t, e, "admin", "domain2", []string{"bob"}) + + testGetUsers(t, e, []string{}, "non_exist", "domain2") + testGetUsersInDomain(t, e, "non_exist", "domain2", []string{}) + + _, _ = e.DeleteRoleForUserInDomain("alice", "admin", "domain1") + _, _ = e.AddRoleForUserInDomain("bob", "admin", "domain1") + + testGetUsers(t, e, []string{"bob"}, "admin", "domain1") + testGetUsersInDomain(t, e, "admin", "domain1", []string{"bob"}) + + testGetUsers(t, e, []string{}, "non_exist", "domain1") + testGetUsersInDomain(t, e, "non_exist", "domain1", []string{}) + + testGetUsers(t, e, []string{"bob"}, "admin", "domain2") + testGetUsersInDomain(t, e, "admin", "domain2", []string{"bob"}) + + testGetUsers(t, e, []string{}, "non_exist", "domain2") + testGetUsersInDomain(t, e, "non_exist", "domain2", []string{}) +} + +func TestRoleAPIWithDomains(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testGetRoles(t, e, []string{"admin"}, "alice", "domain1") + testGetRolesInDomain(t, e, "alice", "domain1", []string{"admin"}) + + testGetRoles(t, e, []string{}, "bob", "domain1") + testGetRolesInDomain(t, e, "bob", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "admin", "domain1") + testGetRolesInDomain(t, e, "admin", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain1") + testGetRolesInDomain(t, e, "non_exist", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "alice", "domain2") + testGetRolesInDomain(t, e, "alice", "domain2", []string{}) + + testGetRoles(t, e, []string{"admin"}, "bob", "domain2") + testGetRolesInDomain(t, e, "bob", "domain2", []string{"admin"}) + + testGetRoles(t, e, []string{}, "admin", "domain2") + testGetRolesInDomain(t, e, "admin", "domain2", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain2") + testGetRolesInDomain(t, e, "non_exist", "domain2", []string{}) + + _, _ = e.DeleteRoleForUserInDomain("alice", "admin", "domain1") + _, _ = e.AddRoleForUserInDomain("bob", "admin", "domain1") + + testGetRoles(t, e, []string{}, "alice", "domain1") + testGetRolesInDomain(t, e, "alice", "domain1", []string{}) + + testGetRoles(t, e, []string{"admin"}, "bob", "domain1") + testGetRolesInDomain(t, e, "bob", "domain1", []string{"admin"}) + + testGetRoles(t, e, []string{}, "admin", "domain1") + testGetRolesInDomain(t, e, "admin", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain1") + testGetRolesInDomain(t, e, "non_exist", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "alice", "domain2") + testGetRolesInDomain(t, e, "alice", "domain2", []string{}) + + testGetRoles(t, e, []string{"admin"}, "bob", "domain2") + testGetRolesInDomain(t, e, "bob", "domain2", []string{"admin"}) + + testGetRoles(t, e, []string{}, "admin", "domain2") + testGetRolesInDomain(t, e, "admin", "domain2", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain2") + testGetRolesInDomain(t, e, "non_exist", "domain2", []string{}) + + _, _ = e.AddRoleForUserInDomain("alice", "admin", "domain1") + _, _ = e.DeleteRolesForUserInDomain("bob", "domain1") + + testGetRoles(t, e, []string{"admin"}, "alice", "domain1") + testGetRolesInDomain(t, e, "alice", "domain1", []string{"admin"}) + + testGetRoles(t, e, []string{}, "bob", "domain1") + testGetRolesInDomain(t, e, "bob", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "admin", "domain1") + testGetRolesInDomain(t, e, "admin", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain1") + testGetRolesInDomain(t, e, "non_exist", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "alice", "domain2") + testGetRolesInDomain(t, e, "alice", "domain2", []string{}) + + testGetRoles(t, e, []string{"admin"}, "bob", "domain2") + testGetRolesInDomain(t, e, "bob", "domain2", []string{"admin"}) + + testGetRoles(t, e, []string{}, "admin", "domain2") + testGetRolesInDomain(t, e, "admin", "domain2", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain2") + testGetRolesInDomain(t, e, "non_exist", "domain2", []string{}) +} + +func testGetPermissionsInDomain(t *testing.T, e *Enforcer, name string, domain string, res [][]string) { + t.Helper() + myRes := e.GetPermissionsForUserInDomain(name, domain) + t.Log("Permissions for ", name, " under ", domain, ": ", myRes) + + if !util.Array2DEquals(res, myRes) { + t.Error("Permissions for ", name, " under ", domain, ": ", myRes, ", supposed to be ", res) + } +} + +func TestPermissionAPIInDomain(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testGetPermissionsInDomain(t, e, "alice", "domain1", [][]string{{"admin", "domain1", "data1", "read"}, {"admin", "domain1", "data1", "write"}}) + testGetPermissionsInDomain(t, e, "bob", "domain1", [][]string{}) + testGetPermissionsInDomain(t, e, "admin", "domain1", [][]string{{"admin", "domain1", "data1", "read"}, {"admin", "domain1", "data1", "write"}}) + testGetPermissionsInDomain(t, e, "non_exist", "domain1", [][]string{}) + + testGetPermissionsInDomain(t, e, "alice", "domain2", [][]string{}) + testGetPermissionsInDomain(t, e, "bob", "domain2", [][]string{{"admin", "domain2", "data2", "read"}, {"admin", "domain2", "data2", "write"}}) + testGetPermissionsInDomain(t, e, "admin", "domain2", [][]string{{"admin", "domain2", "data2", "read"}, {"admin", "domain2", "data2", "write"}}) + testGetPermissionsInDomain(t, e, "non_exist", "domain2", [][]string{}) +} + +func testGetDomainsForUser(t *testing.T, e *Enforcer, res []string, user string) { + t.Helper() + myRes, _ := e.GetDomainsForUser(user) + + sort.Strings(myRes) + sort.Strings(res) + + if !util.SetEquals(res, myRes) { + t.Error("domains for user: ", user, ": ", myRes, ", supposed to be ", res) + } +} + +func TestGetDomainsForUser(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy2.csv") + + testGetDomainsForUser(t, e, []string{"domain1", "domain2"}, "alice") + testGetDomainsForUser(t, e, []string{"domain2", "domain3"}, "bob") + testGetDomainsForUser(t, e, []string{"domain3"}, "user") +} + +func testGetAllUsersByDomain(t *testing.T, e *Enforcer, domain string, expected []string) { + users, _ := e.GetAllUsersByDomain(domain) + if !util.SetEquals(users, expected) { + t.Errorf("users in %s: %v, supposed to be %v\n", domain, users, expected) + } +} + +func TestGetAllUsersByDomain(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testGetAllUsersByDomain(t, e, "domain1", []string{"alice", "admin"}) + testGetAllUsersByDomain(t, e, "domain2", []string{"bob", "admin"}) +} + +func testDeleteAllUsersByDomain(t *testing.T, domain string, expectedPolicy, expectedGroupingPolicy [][]string) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + _, _ = e.DeleteAllUsersByDomain(domain) + policy, err := e.GetPolicy() + if err != nil { + t.Error(err) + } + if !util.Array2DEquals(policy, expectedPolicy) { + t.Errorf("policy in %s: %v, supposed to be %v\n", domain, policy, expectedPolicy) + } + + policies, err := e.GetGroupingPolicy() + if err != nil { + t.Error(err) + } + if !util.Array2DEquals(policies, expectedGroupingPolicy) { + t.Errorf("grouping policy in %s: %v, supposed to be %v\n", domain, policies, expectedGroupingPolicy) + } +} + +func TestDeleteAllUsersByDomain(t *testing.T) { + testDeleteAllUsersByDomain(t, "domain1", [][]string{ + {"admin", "domain2", "data2", "read"}, + {"admin", "domain2", "data2", "write"}, + }, [][]string{ + {"bob", "admin", "domain2"}, + }) + testDeleteAllUsersByDomain(t, "domain2", [][]string{ + {"admin", "domain1", "data1", "read"}, + {"admin", "domain1", "data1", "write"}, + }, [][]string{ + {"alice", "admin", "domain1"}, + }) +} + +// testGetAllDomains tests GetAllDomains(). +func testGetAllDomains(t *testing.T, e *Enforcer, res []string) { + t.Helper() + myRes, _ := e.GetAllDomains() + sort.Strings(myRes) + sort.Strings(res) + if !util.ArrayEquals(res, myRes) { + t.Error("domains: ", myRes, ", supposed to be ", res) + } +} + +func TestGetAllDomains(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testGetAllDomains(t, e, []string{"domain1", "domain2"}) +} + +func testGetAllRolesByDomain(t *testing.T, e *Enforcer, domain string, expected []string) { + roles, _ := e.GetAllRolesByDomain(domain) + if !util.SetEquals(roles, expected) { + t.Errorf("roles in %s: %v, supposed to be %v\n", domain, roles, expected) + } +} + +func TestGetAllRolesByDomain(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + testGetAllRolesByDomain(t, e, "domain1", []string{"admin"}) + testGetAllRolesByDomain(t, e, "domain2", []string{"admin"}) + + e, _ = NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy2.csv") + + testGetAllRolesByDomain(t, e, "domain1", []string{"admin"}) + testGetAllRolesByDomain(t, e, "domain2", []string{"admin"}) + testGetAllRolesByDomain(t, e, "domain3", []string{"user"}) +} + +func testDeleteDomains(t *testing.T, domains []string, expectedPolicy, expectedGroupingPolicy [][]string, expectedDomains []string) { + e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + + _, _ = e.DeleteDomains(domains...) + policy, err := e.GetPolicy() + if err != nil { + t.Error(err) + } + if !util.Array2DEquals(policy, expectedPolicy) { + t.Errorf("policy after deleting domains %v: %v, supposed to be %v\n", domains, policy, expectedPolicy) + } + + policies, err := e.GetGroupingPolicy() + if err != nil { + t.Error(err) + } + if !util.Array2DEquals(policies, expectedGroupingPolicy) { + t.Errorf("grouping policy after deleting domains %v: %v, supposed to be %v\n", domains, policies, expectedGroupingPolicy) + } + + domainsAfterRemoval, _ := e.GetAllDomains() + if !util.SetEquals(domainsAfterRemoval, expectedDomains) { + t.Errorf("domains after deleting %v: %v, supposed to be %v\n", domains, domainsAfterRemoval, expectedDomains) + } +} + +func TestDeleteDomains(t *testing.T) { + testDeleteDomains(t, []string{"domain1"}, [][]string{ + {"admin", "domain2", "data2", "read"}, + {"admin", "domain2", "data2", "write"}, + }, [][]string{ + {"bob", "admin", "domain2"}, + }, []string{"domain2"}) + + testDeleteDomains(t, []string{"domain2"}, [][]string{ + {"admin", "domain1", "data1", "read"}, + {"admin", "domain1", "data1", "write"}, + }, [][]string{ + {"alice", "admin", "domain1"}, + }, []string{"domain1"}) + + testDeleteDomains(t, []string{}, [][]string{}, [][]string{}, []string{}) +} + +// TestGetRolesForUserInDomainWithConditionalFunctions. +func TestGetRolesForUserInDomainWithConditionalFunctions(t *testing.T) { + modelText := "examples/rbac_with_domains_conditional_model.conf" + policyText := "examples/rbac_with_domains_conditional_policy.csv" + + e, err := NewEnforcer(modelText, policyText) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Test without conditional functions + t.Run("WithoutConditionalFunctions", func(t *testing.T) { + roles := e.GetRolesForUserInDomain("alice", "domain1") + expected := []string{"test1"} + if !util.SetEquals(roles, expected) { + t.Errorf("Expected roles %v, got %v", expected, roles) + } + }) + + t.Run("WithConditionalFunctions", func(t *testing.T) { + e2, err := NewEnforcer(modelText, policyText) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Add time-based conditional functions + g, err := e2.GetNamedGroupingPolicy("g") + if err != nil { + t.Fatalf("Failed to get grouping policy: %v", err) + } + for _, gp := range g { + if len(gp) >= 4 { + e2.AddNamedDomainLinkConditionFunc("g", gp[0], gp[1], gp[2], util.TimeMatchFunc) + e2.SetNamedDomainLinkConditionFuncParams("g", gp[0], gp[1], gp[2], "_", gp[4]) + } + } + + roles := e2.GetRolesForUserInDomain("alice", "domain1") + if roles == nil { + t.Error("GetRolesForUserInDomain should not return nil, even with conditional functions") + } + + roles = e2.GetRolesForUserInDomain("bob", "domain2") + if roles == nil { + t.Error("GetRolesForUserInDomain should not return nil for bob, even with conditional functions") + } + }) + + t.Run("WithAlwaysTrueCondition", func(t *testing.T) { + e3, err := NewEnforcer(modelText, policyText) + if err != nil { + t.Fatalf("Failed to create enforcer: %v", err) + } + + // Use always-true condition function + alwaysTrueFunc := func(params ...string) (bool, error) { + return true, nil + } + + g, err := e3.GetNamedGroupingPolicy("g") + if err != nil { + t.Fatalf("Failed to get grouping policy: %v", err) + } + for _, gp := range g { + if len(gp) >= 4 { + e3.AddNamedDomainLinkConditionFunc("g", gp[0], gp[1], gp[2], alwaysTrueFunc) + } + } + + roles := e3.GetRolesForUserInDomain("alice", "domain1") + expected := []string{"test1"} + if !util.SetEquals(roles, expected) { + t.Errorf("Expected roles %v, got %v", expected, roles) + } + + roles = e3.GetRolesForUserInDomain("bob", "domain2") + expected = []string{"qa1"} + if !util.SetEquals(roles, expected) { + t.Errorf("Expected roles %v, got %v", expected, roles) + } + }) +} diff --git a/rbac_with_abac_rule_test.go b/rbac_with_abac_rule_test.go new file mode 100644 index 0000000..7a4e1c1 --- /dev/null +++ b/rbac_with_abac_rule_test.go @@ -0,0 +1,129 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +// testCtx holds the request-time attributes that the cond column of +// examples/rbac_with_abac_rule_policy.csv reads. A struct is the safe shape for +// a context: every field always exists, so a policy can never reference an +// attribute the request forgot to supply. See TestRBACWithABACRuleMissingAttr +// for what happens when it does. +type testCtx struct { + Hour int + Secure bool + Risk string +} + +// Helper function for RBAC-with-ABAC-condition enforcement testing. +func testEnforceRBACWithABACRule(t *testing.T, e *Enforcer, sub string, obj string, act string, ctx interface{}, res bool) { + t.Helper() + myRes, err := e.Enforce(sub, obj, act, ctx) + if err != nil { + t.Errorf("Enforce Error: %s", err) + return + } + if myRes != res { + t.Errorf("%s, %s, %s, %v: %t, supposed to be %t", sub, obj, act, ctx, myRes, res) + } +} + +// TestRBACWithABACRule covers a model where roles decide *what* a subject may +// touch and a per-policy condition decides *under which circumstances*: +// +// m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act) && eval(p.cond) +// +// The deny-override effect lets a single policy line veto every allow above it. +func TestRBACWithABACRule(t *testing.T) { + e, err := NewEnforcer("examples/rbac_with_abac_rule_model.conf", "examples/rbac_with_abac_rule_policy.csv") + if err != nil { + t.Fatalf("NewEnforcer failed: %v", err) + } + + // alice is an admin, and admin inherits employee; bob is a plain employee. + office := testCtx{Hour: 10, Secure: true, Risk: "low"} + afterHours := testCtx{Hour: 20, Secure: true, Risk: "low"} + insecure := testCtx{Hour: 10, Secure: false, Risk: "low"} + risky := testCtx{Hour: 10, Secure: true, Risk: "high"} + + // data1/read is allowed to employees during business hours only. + testEnforceRBACWithABACRule(t, e, "alice", "data1", "read", office, true) + testEnforceRBACWithABACRule(t, e, "bob", "data1", "read", office, true) + testEnforceRBACWithABACRule(t, e, "alice", "data1", "read", afterHours, false) + testEnforceRBACWithABACRule(t, e, "bob", "data1", "read", afterHours, false) + + // No policy grants data1/write, so the condition never gets a say. + testEnforceRBACWithABACRule(t, e, "alice", "data1", "write", office, false) + + // data2/write requires a secure connection; data2/read is not granted. + testEnforceRBACWithABACRule(t, e, "bob", "data2", "write", office, true) + testEnforceRBACWithABACRule(t, e, "bob", "data2", "write", insecure, false) + testEnforceRBACWithABACRule(t, e, "bob", "data2", "read", office, false) + + // data3/read is unconditional ("true") but restricted to the admin role. + testEnforceRBACWithABACRule(t, e, "alice", "data3", "read", office, true) + testEnforceRBACWithABACRule(t, e, "alice", "data3", "read", afterHours, true) + testEnforceRBACWithABACRule(t, e, "bob", "data3", "read", office, false) + + // The wildcard deny line outranks every allow, including unconditional ones. + testEnforceRBACWithABACRule(t, e, "alice", "data1", "read", risky, false) + testEnforceRBACWithABACRule(t, e, "alice", "data3", "read", risky, false) + testEnforceRBACWithABACRule(t, e, "bob", "data2", "write", risky, false) + + // carol holds no role, and data4 is covered by no policy. + testEnforceRBACWithABACRule(t, e, "carol", "data1", "read", office, false) + testEnforceRBACWithABACRule(t, e, "alice", "data4", "read", office, false) +} + +// TestRBACWithABACRuleMapCtx shows that a map works as the context too, as long +// as it carries every attribute the matching policies read. +func TestRBACWithABACRuleMapCtx(t *testing.T) { + e, err := NewEnforcer("examples/rbac_with_abac_rule_model.conf", "examples/rbac_with_abac_rule_policy.csv") + if err != nil { + t.Fatalf("NewEnforcer failed: %v", err) + } + + office := map[string]interface{}{"Hour": 10, "Secure": true, "Risk": "low"} + risky := map[string]interface{}{"Hour": 10, "Secure": true, "Risk": "high"} + + testEnforceRBACWithABACRule(t, e, "alice", "data1", "read", office, true) + testEnforceRBACWithABACRule(t, e, "alice", "data1", "read", risky, false) +} + +// TestRBACWithABACRuleMissingAttr pins down the one sharp edge of putting +// expressions in policies: a cond that reads an attribute the request context +// does not carry is an evaluation error, not a denial. Callers must not treat +// the returned false as "denied" without checking err. +func TestRBACWithABACRuleMissingAttr(t *testing.T) { + e, err := NewEnforcer("examples/rbac_with_abac_rule_model.conf", "examples/rbac_with_abac_rule_policy.csv") + if err != nil { + t.Fatalf("NewEnforcer failed: %v", err) + } + + // The data1/read policy reads r.ctx.Hour, which this map omits. + if _, err := e.Enforce("bob", "data1", "read", map[string]interface{}{"Secure": true, "Risk": "low"}); err == nil { + t.Error("Enforce: expected an error when the context omits an attribute used by a policy condition, got nil") + } + + // Same for a nil context: there is nothing to read attributes from. + if _, err := e.Enforce("bob", "data1", "read", nil); err == nil { + t.Error("Enforce: expected an error for a nil context, got nil") + } +} diff --git a/role_manager_b_test.go b/role_manager_b_test.go new file mode 100644 index 0000000..5a933e3 --- /dev/null +++ b/role_manager_b_test.go @@ -0,0 +1,212 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + "testing" + + "github.com/casbin/casbin/v3/util" +) + +func BenchmarkRoleManagerSmall(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_model.conf") + // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call. + e.EnableAutoBuildRoleLinks(false) + + // 100 roles, 10 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 100; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 1000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 1000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + rm := e.GetRoleManager() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 100; j++ { + _, _ = rm.HasLink("user501", fmt.Sprintf("group%d", j)) + } + } +} + +func BenchmarkRoleManagerMedium(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_model.conf") + // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call. + e.EnableAutoBuildRoleLinks(false) + + // 1000 roles, 100 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 1000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 10000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + err = e.BuildRoleLinks() + if err != nil { + b.Fatal(err) + } + + rm := e.GetRoleManager() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 1000; j++ { + _, _ = rm.HasLink("user501", fmt.Sprintf("group%d", j)) + } + } +} + +func BenchmarkRoleManagerLarge(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_model.conf") + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) + for i := 0; i < 10000; i++ { + pPolicies = append(pPolicies, []string{fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read"}) + } + + _, err := e.AddPolicies(pPolicies) + if err != nil { + b.Fatal(err) + } + + // 100000 users. + gPolicies := make([][]string, 0) + for i := 0; i < 100000; i++ { + gPolicies = append(gPolicies, []string{fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)}) + } + + _, err = e.AddGroupingPolicies(gPolicies) + if err != nil { + b.Fatal(err) + } + + rm := e.GetRoleManager() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 10000; j++ { + _, _ = rm.HasLink("user501", fmt.Sprintf("group%d", j)) + } + } +} + +func BenchmarkBuildRoleLinksWithPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedMatchingFunc("g", "", util.KeyMatch4) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = e.BuildRoleLinks() + } +} + +func BenchmarkBuildRoleLinksWithDomainPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedDomainMatchingFunc("g", "", util.KeyMatch4) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = e.BuildRoleLinks() + } +} + +func BenchmarkBuildRoleLinksWithPatternAndDomainPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedMatchingFunc("g", "", util.KeyMatch4) + e.AddNamedDomainMatchingFunc("g", "", util.KeyMatch4) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = e.BuildRoleLinks() + } +} + +func BenchmarkHasLinkWithPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedMatchingFunc("g", "", util.KeyMatch4) + rm := e.rmMap["g"] + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = rm.HasLink("staffUser1001", "staff001", "/orgs/1/sites/site001") + } +} + +func BenchmarkHasLinkWithDomainPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedDomainMatchingFunc("g", "", util.KeyMatch4) + rm := e.rmMap["g"] + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = rm.HasLink("staffUser1001", "staff001", "/orgs/1/sites/site001") + } +} + +func BenchmarkHasLinkWithPatternAndDomainPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedMatchingFunc("g", "", util.KeyMatch4) + e.AddNamedDomainMatchingFunc("g", "", util.KeyMatch4) + rm := e.rmMap["g"] + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = rm.HasLink("staffUser1001", "staff001", "/orgs/1/sites/site001") + } +} + +// BenchmarkConcurrentHasLinkWithMatching benchmarks concurrent HasLink performance with matching functions. +// Performance test for concurrent access with temporary role creation. +func BenchmarkConcurrentHasLinkWithMatching(b *testing.B) { + e, _ := NewEnforcer("examples/rbac_with_pattern_model.conf", "examples/rbac_with_pattern_policy.csv") + e.AddNamedMatchingFunc("g2", "keyMatch2", util.KeyMatch2) + rm := e.GetRoleManager() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, _ = rm.HasLink("alice", "/book/123") + } + }) +} diff --git a/syntax_test.go b/syntax_test.go new file mode 100644 index 0000000..45b9dbb --- /dev/null +++ b/syntax_test.go @@ -0,0 +1,37 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +// TestSyntaxMatcher tests that patterns like "p." inside quoted strings +// are not incorrectly escaped by EscapeAssertion. +// This addresses the bug where matchers containing strings like "a.p.p.l.e" +// would have the ".p." pattern incorrectly replaced with "_p_". +func TestSyntaxMatcher(t *testing.T) { + e, err := NewEnforcer("examples/syntax_matcher_model.conf") + if err != nil { + t.Fatalf("Error: %v\n", err) + } + // The matcher in syntax_matcher_model.conf is: m = r.sub == "a.p.p.l.e" + // This should match when r.sub is exactly "a.p.p.l.e" + testEnforce(t, e, "a.p.p.l.e", "file", "read", true) + testEnforce(t, e, "other", "file", "read", false) +} diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..eb08512 --- /dev/null +++ b/transaction.go @@ -0,0 +1,485 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +const ( + // Default timeout duration for lock acquisition. + defaultLockTimeout = 30 * time.Second +) + +// Transaction represents a Casbin transaction. +// It provides methods to perform policy operations within a transaction. +// and commit or rollback all changes atomically. +type Transaction struct { + id string // Unique transaction identifier. + enforcer *TransactionalEnforcer // Reference to the transactional enforcer. + buffer *TransactionBuffer // Buffer for policy operations. + txContext persist.TransactionContext // Database transaction context. + ctx context.Context // Context for the transaction. + baseVersion int64 // Model version at transaction start. + committed bool // Whether the transaction has been committed. + rolledBack bool // Whether the transaction has been rolled back. + startTime time.Time // Transaction start timestamp. + mutex sync.RWMutex // Protects transaction state. +} + +// GetAdapter returns the adapter that operates within this transaction. +// Writes done through it join the same database transaction as the buffered +// policy changes, so non-policy operations can be made atomic together with the +// policy changes, e.g. with the GORM adapter: +// +// err := e.WithTransaction(ctx, func(tx *casbin.Transaction) error { +// db := tx.GetAdapter().(*gormadapter.Adapter).GetDb(). +// Session(&gorm.Session{NewDB: true}) +// if err := db.Where("name = ?", "admin").Delete(&Role{}).Error; err != nil { +// return err +// } +// _, err := tx.RemoveGroupingPolicy("alice", "admin") +// return err +// }) +// +// Mind the Session(&gorm.Session{NewDB: true}): the GORM adapter pins its +// *gorm.DB to the policy table, and without resetting the statement every query +// would silently run against casbin_rule instead of the caller's own table. +func (tx *Transaction) GetAdapter() persist.Adapter { + return tx.txContext.GetAdapter() +} + +// buildRuleFromParams converts parameters to a rule slice. +func (tx *Transaction) buildRuleFromParams(params ...interface{}) []string { + if len(params) == 1 { + if strSlice, ok := params[0].([]string); ok { + rule := make([]string, 0, len(strSlice)) + rule = append(rule, strSlice...) + return rule + } + } + + rule := make([]string, 0, len(params)) + for _, param := range params { + rule = append(rule, param.(string)) + } + return rule +} + +// checkTransactionStatus checks if the transaction is active. +func (tx *Transaction) checkTransactionStatus() error { + if tx.committed || tx.rolledBack { + return errors.New("transaction is not active") + } + return nil +} + +// getBufferedModel returns the model with all buffered operations applied. +// The caller must hold tx.mutex. +func (tx *Transaction) getBufferedModel() (model.Model, error) { + if err := tx.checkTransactionStatus(); err != nil { + return nil, err + } + + return tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot()) +} + +// bufferAdd buffers an add operation for the rules that are not present yet. +// Rules that already exist in the buffered model are skipped, and false is +// returned when nothing is left to add. +func (tx *Transaction) bufferAdd(sec string, ptype string, rules [][]string) (bool, error) { + tx.mutex.Lock() + defer tx.mutex.Unlock() + + if len(rules) == 0 { + if err := tx.checkTransactionStatus(); err != nil { + return false, err + } + return false, nil + } + + bufferedModel, err := tx.getBufferedModel() + if err != nil { + return false, err + } + + var validRules [][]string + for _, rule := range rules { + hasPolicy, err := bufferedModel.HasPolicy(sec, ptype, rule) + if err != nil { + return false, err + } + if !hasPolicy { + validRules = append(validRules, rule) + } + } + + if len(validRules) == 0 { + return false, nil + } + + tx.buffer.AddOperation(persist.PolicyOperation{ + Type: persist.OperationAdd, + Section: sec, + PolicyType: ptype, + Rules: validRules, + }) + + return true, nil +} + +// bufferRemove buffers a remove operation for the rules that are present. +// Rules that are missing from the buffered model are skipped, and false is +// returned when nothing is left to remove. +func (tx *Transaction) bufferRemove(sec string, ptype string, rules [][]string) (bool, error) { + tx.mutex.Lock() + defer tx.mutex.Unlock() + + if len(rules) == 0 { + if err := tx.checkTransactionStatus(); err != nil { + return false, err + } + return false, nil + } + + bufferedModel, err := tx.getBufferedModel() + if err != nil { + return false, err + } + + var validRules [][]string + for _, rule := range rules { + hasPolicy, err := bufferedModel.HasPolicy(sec, ptype, rule) + if err != nil { + return false, err + } + if hasPolicy { + validRules = append(validRules, rule) + } + } + + if len(validRules) == 0 { + return false, nil + } + + tx.buffer.AddOperation(persist.PolicyOperation{ + Type: persist.OperationRemove, + Section: sec, + PolicyType: ptype, + Rules: validRules, + }) + + return true, nil +} + +// bufferRemoveFiltered resolves the filter against the buffered model and +// buffers the matched rules for removal. Resolving the filter here keeps the +// operation a plain remove, so it works with every adapter and takes part in +// conflict detection like any other remove. +func (tx *Transaction) bufferRemoveFiltered(sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) { + tx.mutex.Lock() + defer tx.mutex.Unlock() + + bufferedModel, err := tx.getBufferedModel() + if err != nil { + return false, err + } + + matched, err := bufferedModel.GetFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + if err != nil { + return false, err + } + if len(matched) == 0 { + return false, nil + } + + // The matched rules belong to a throwaway model copy, so keep our own copy. + rules := make([][]string, 0, len(matched)) + for _, rule := range matched { + rules = append(rules, append([]string(nil), rule...)) + } + + tx.buffer.AddOperation(persist.PolicyOperation{ + Type: persist.OperationRemove, + Section: sec, + PolicyType: ptype, + Rules: rules, + }) + + return true, nil +} + +// bufferUpdate buffers an update operation. The update is all-or-nothing: it is +// buffered only if every old rule exists and no new rule exists yet. +func (tx *Transaction) bufferUpdate(sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + tx.mutex.Lock() + defer tx.mutex.Unlock() + + if len(oldRules) != len(newRules) { + return false, fmt.Errorf("the length of oldRules should be equal to the length of newRules, but got the length of oldRules is %d, the length of newRules is %d", len(oldRules), len(newRules)) + } + + if len(oldRules) == 0 { + if err := tx.checkTransactionStatus(); err != nil { + return false, err + } + return false, nil + } + + bufferedModel, err := tx.getBufferedModel() + if err != nil { + return false, err + } + + for i, oldRule := range oldRules { + hasOldPolicy, err := bufferedModel.HasPolicy(sec, ptype, oldRule) + if err != nil { + return false, err + } + if !hasOldPolicy { + return false, nil + } + + hasNewPolicy, err := bufferedModel.HasPolicy(sec, ptype, newRules[i]) + if err != nil { + return false, err + } + if hasNewPolicy { + return false, nil + } + } + + tx.buffer.AddOperation(persist.PolicyOperation{ + Type: persist.OperationUpdate, + Section: sec, + PolicyType: ptype, + Rules: newRules, + OldRules: oldRules, + }) + + return true, nil +} + +// AddPolicy adds a policy within the transaction. +// The policy is buffered and will be applied when the transaction is committed. +func (tx *Transaction) AddPolicy(params ...interface{}) (bool, error) { + return tx.AddNamedPolicy("p", params...) +} + +// AddNamedPolicy adds a named policy within the transaction. +// The policy is buffered and will be applied when the transaction is committed. +func (tx *Transaction) AddNamedPolicy(ptype string, params ...interface{}) (bool, error) { + return tx.bufferAdd("p", ptype, [][]string{tx.buildRuleFromParams(params...)}) +} + +// AddPolicies adds multiple policies within the transaction. +func (tx *Transaction) AddPolicies(rules [][]string) (bool, error) { + return tx.AddNamedPolicies("p", rules) +} + +// AddNamedPolicies adds multiple named policies within the transaction. +func (tx *Transaction) AddNamedPolicies(ptype string, rules [][]string) (bool, error) { + return tx.bufferAdd("p", ptype, rules) +} + +// RemovePolicy removes a policy within the transaction. +func (tx *Transaction) RemovePolicy(params ...interface{}) (bool, error) { + return tx.RemoveNamedPolicy("p", params...) +} + +// RemoveNamedPolicy removes a named policy within the transaction. +func (tx *Transaction) RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error) { + return tx.bufferRemove("p", ptype, [][]string{tx.buildRuleFromParams(params...)}) +} + +// RemovePolicies removes multiple policies within the transaction. +func (tx *Transaction) RemovePolicies(rules [][]string) (bool, error) { + return tx.RemoveNamedPolicies("p", rules) +} + +// RemoveNamedPolicies removes multiple named policies within the transaction. +func (tx *Transaction) RemoveNamedPolicies(ptype string, rules [][]string) (bool, error) { + return tx.bufferRemove("p", ptype, rules) +} + +// RemoveFilteredPolicy removes the policies that match the filter within the transaction. +// An empty string in fieldValues means "match any value" for that field. +func (tx *Transaction) RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error) { + return tx.RemoveFilteredNamedPolicy("p", fieldIndex, fieldValues...) +} + +// RemoveFilteredNamedPolicy removes the named policies that match the filter within the transaction. +// An empty string in fieldValues means "match any value" for that field. +func (tx *Transaction) RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return tx.bufferRemoveFiltered("p", ptype, fieldIndex, fieldValues) +} + +// UpdatePolicy updates a policy within the transaction. +func (tx *Transaction) UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error) { + return tx.UpdateNamedPolicy("p", oldPolicy, newPolicy) +} + +// UpdateNamedPolicy updates a named policy within the transaction. +func (tx *Transaction) UpdateNamedPolicy(ptype string, oldPolicy []string, newPolicy []string) (bool, error) { + return tx.bufferUpdate("p", ptype, [][]string{oldPolicy}, [][]string{newPolicy}) +} + +// UpdatePolicies updates multiple policies within the transaction. +// Nothing is buffered unless every old policy exists and no new policy exists yet. +func (tx *Transaction) UpdatePolicies(oldPolicies [][]string, newPolicies [][]string) (bool, error) { + return tx.UpdateNamedPolicies("p", oldPolicies, newPolicies) +} + +// UpdateNamedPolicies updates multiple named policies within the transaction. +// Nothing is buffered unless every old policy exists and no new policy exists yet. +func (tx *Transaction) UpdateNamedPolicies(ptype string, oldPolicies [][]string, newPolicies [][]string) (bool, error) { + return tx.bufferUpdate("p", ptype, oldPolicies, newPolicies) +} + +// AddGroupingPolicy adds a grouping policy within the transaction. +func (tx *Transaction) AddGroupingPolicy(params ...interface{}) (bool, error) { + return tx.AddNamedGroupingPolicy("g", params...) +} + +// AddNamedGroupingPolicy adds a named grouping policy within the transaction. +func (tx *Transaction) AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + return tx.bufferAdd("g", ptype, [][]string{tx.buildRuleFromParams(params...)}) +} + +// AddGroupingPolicies adds multiple grouping policies within the transaction. +func (tx *Transaction) AddGroupingPolicies(rules [][]string) (bool, error) { + return tx.AddNamedGroupingPolicies("g", rules) +} + +// AddNamedGroupingPolicies adds multiple named grouping policies within the transaction. +func (tx *Transaction) AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + return tx.bufferAdd("g", ptype, rules) +} + +// RemoveGroupingPolicy removes a grouping policy within the transaction. +func (tx *Transaction) RemoveGroupingPolicy(params ...interface{}) (bool, error) { + return tx.RemoveNamedGroupingPolicy("g", params...) +} + +// RemoveNamedGroupingPolicy removes a named grouping policy within the transaction. +func (tx *Transaction) RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + return tx.bufferRemove("g", ptype, [][]string{tx.buildRuleFromParams(params...)}) +} + +// RemoveGroupingPolicies removes multiple grouping policies within the transaction. +func (tx *Transaction) RemoveGroupingPolicies(rules [][]string) (bool, error) { + return tx.RemoveNamedGroupingPolicies("g", rules) +} + +// RemoveNamedGroupingPolicies removes multiple named grouping policies within the transaction. +func (tx *Transaction) RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + return tx.bufferRemove("g", ptype, rules) +} + +// RemoveFilteredGroupingPolicy removes the grouping policies that match the filter within the transaction. +// An empty string in fieldValues means "match any value" for that field. +func (tx *Transaction) RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error) { + return tx.RemoveFilteredNamedGroupingPolicy("g", fieldIndex, fieldValues...) +} + +// RemoveFilteredNamedGroupingPolicy removes the named grouping policies that match +// the filter within the transaction. +// An empty string in fieldValues means "match any value" for that field. +func (tx *Transaction) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + return tx.bufferRemoveFiltered("g", ptype, fieldIndex, fieldValues) +} + +// UpdateGroupingPolicy updates a grouping policy within the transaction. +func (tx *Transaction) UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) { + return tx.UpdateNamedGroupingPolicy("g", oldRule, newRule) +} + +// UpdateNamedGroupingPolicy updates a named grouping policy within the transaction. +func (tx *Transaction) UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error) { + return tx.bufferUpdate("g", ptype, [][]string{oldRule}, [][]string{newRule}) +} + +// UpdateGroupingPolicies updates multiple grouping policies within the transaction. +// Nothing is buffered unless every old rule exists and no new rule exists yet. +func (tx *Transaction) UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error) { + return tx.UpdateNamedGroupingPolicies("g", oldRules, newRules) +} + +// UpdateNamedGroupingPolicies updates multiple named grouping policies within the transaction. +// Nothing is buffered unless every old rule exists and no new rule exists yet. +func (tx *Transaction) UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + return tx.bufferUpdate("g", ptype, oldRules, newRules) +} + +// GetBufferedModel returns the model as it would look after applying all buffered operations. +// This is useful for preview or validation purposes within the transaction. +func (tx *Transaction) GetBufferedModel() (model.Model, error) { + tx.mutex.RLock() + defer tx.mutex.RUnlock() + + return tx.getBufferedModel() +} + +// HasOperations returns true if the transaction has any buffered operations. +func (tx *Transaction) HasOperations() bool { + tx.mutex.RLock() + defer tx.mutex.RUnlock() + return tx.buffer.HasOperations() +} + +// OperationCount returns the number of buffered operations in the transaction. +func (tx *Transaction) OperationCount() int { + tx.mutex.RLock() + defer tx.mutex.RUnlock() + return tx.buffer.OperationCount() +} + +// tryLockWithTimeout attempts to acquire the lock within the specified timeout period. +func tryLockWithTimeout(lock *sync.Mutex, startTime time.Time, maxWait time.Duration) bool { + // Calculate remaining wait time based on transaction start time. + remainingTime := maxWait - time.Since(startTime) + if remainingTime <= 0 { + return false + } + + // Create a context with timeout for lock acquisition. + ctx, cancel := context.WithTimeout(context.Background(), remainingTime) + defer cancel() + + // Use channel for timeout control. + done := make(chan bool, 1) + go func() { + lock.Lock() + done <- true + }() + + // Wait for either lock acquisition or timeout. + select { + case <-done: + return true + case <-ctx.Done(): + return false + } +} diff --git a/transaction_adapter_test.go b/transaction_adapter_test.go new file mode 100644 index 0000000..e38fa4d --- /dev/null +++ b/transaction_adapter_test.go @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "context" + "testing" +) + +// GetAdapter hands out the transaction scoped adapter, so the caller can run +// its own database operations in the same transaction as the policy changes. +// See https://github.com/casbin/casbin/issues/1706. +func TestTransactionGetAdapter(t *testing.T) { + adapter := NewMockTransactionalAdapter() + e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter) + if err != nil { + t.Fatalf("Failed to create transactional enforcer: %v", err) + } + + err = e.WithTransaction(context.Background(), func(tx *Transaction) error { + // The adapter of the transaction, not the one of the enforcer: for a + // real adapter this is the one bound to the open database transaction. + if tx.GetAdapter() == nil { + t.Error("The transaction scoped adapter should be reachable") + } + _, addErr := tx.AddPolicy("admin", "data1", "read") + return addErr + }) + if err != nil { + t.Fatalf("Failed to run transaction: %v", err) + } + + if ok, err := e.HasPolicy("admin", "data1", "read"); err != nil || !ok { + t.Fatalf("The policy added in the transaction should be in effect, got %v, %v", ok, err) + } +} diff --git a/transaction_buffer.go b/transaction_buffer.go new file mode 100644 index 0000000..9a8098e --- /dev/null +++ b/transaction_buffer.go @@ -0,0 +1,134 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "sync" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// TransactionBuffer holds all policy changes made within a transaction. +// It maintains a list of operations and a snapshot of the model state +// at the beginning of the transaction. +type TransactionBuffer struct { + operations []persist.PolicyOperation // Buffered operations + modelSnapshot model.Model // Model state at transaction start + mutex sync.RWMutex // Protects concurrent access +} + +// NewTransactionBuffer creates a new transaction buffer with a model snapshot. +// The snapshot represents the state of the model at the beginning of the transaction. +func NewTransactionBuffer(baseModel model.Model) *TransactionBuffer { + return &TransactionBuffer{ + operations: make([]persist.PolicyOperation, 0), + modelSnapshot: baseModel.Copy(), + } +} + +// AddOperation adds a policy operation to the buffer. +// This operation will be applied when the transaction is committed. +func (tb *TransactionBuffer) AddOperation(op persist.PolicyOperation) { + tb.mutex.Lock() + defer tb.mutex.Unlock() + tb.operations = append(tb.operations, op) +} + +// GetOperations returns all buffered operations. +// Returns a copy to prevent external modifications. +func (tb *TransactionBuffer) GetOperations() []persist.PolicyOperation { + tb.mutex.RLock() + defer tb.mutex.RUnlock() + + // Return a copy to prevent external modifications. + result := make([]persist.PolicyOperation, len(tb.operations)) + copy(result, tb.operations) + return result +} + +// Clear removes all buffered operations. +// This is typically called after a successful commit or rollback. +func (tb *TransactionBuffer) Clear() { + tb.mutex.Lock() + defer tb.mutex.Unlock() + tb.operations = tb.operations[:0] +} + +// GetModelSnapshot returns the model snapshot taken at transaction start. +// This represents the original state before any transaction operations. +func (tb *TransactionBuffer) GetModelSnapshot() model.Model { + tb.mutex.RLock() + defer tb.mutex.RUnlock() + return tb.modelSnapshot.Copy() +} + +// ApplyOperationsToModel applies all buffered operations to a model and returns the result. +// This simulates what the model would look like after all operations are applied. +// It's used for validation and preview purposes within the transaction. +func (tb *TransactionBuffer) ApplyOperationsToModel(baseModel model.Model) (model.Model, error) { + tb.mutex.RLock() + defer tb.mutex.RUnlock() + + resultModel := baseModel.Copy() + + for _, op := range tb.operations { + switch op.Type { + case persist.OperationAdd: + for _, rule := range op.Rules { + if err := resultModel.AddPolicy(op.Section, op.PolicyType, rule); err != nil { + return nil, err + } + } + case persist.OperationRemove: + for _, rule := range op.Rules { + if _, err := resultModel.RemovePolicy(op.Section, op.PolicyType, rule); err != nil { + return nil, err + } + } + case persist.OperationUpdate: + // For update operations, remove old rules and add new ones. + for i, oldRule := range op.OldRules { + if i < len(op.Rules) { + if _, err := resultModel.RemovePolicy(op.Section, op.PolicyType, oldRule); err != nil { + return nil, err + } + if err := resultModel.AddPolicy(op.Section, op.PolicyType, op.Rules[i]); err != nil { + return nil, err + } + } + } + } + } + + return resultModel, nil +} + +// HasOperations returns true if there are any buffered operations. +func (tb *TransactionBuffer) HasOperations() bool { + tb.mutex.RLock() + defer tb.mutex.RUnlock() + return len(tb.operations) > 0 +} + +// OperationCount returns the number of buffered operations. +func (tb *TransactionBuffer) OperationCount() int { + tb.mutex.RLock() + defer tb.mutex.RUnlock() + return len(tb.operations) +} diff --git a/transaction_commit.go b/transaction_commit.go new file mode 100644 index 0000000..b400c20 --- /dev/null +++ b/transaction_commit.go @@ -0,0 +1,267 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "errors" + "sync/atomic" + + "github.com/casbin/casbin/v3/persist" +) + +// Commit commits the transaction using a two-phase commit protocol. +// Phase 1: Apply all operations to the database +// Phase 2: Apply changes to the in-memory model and rebuild role links. +func (tx *Transaction) Commit() error { + // Try to acquire the commit lock with timeout. + if !tryLockWithTimeout(&tx.enforcer.commitLock, tx.startTime, defaultLockTimeout) { + _ = tx.txContext.Rollback() + tx.enforcer.activeTransactions.Delete(tx.id) + return errors.New("transaction timeout: failed to acquire lock") + } + defer tx.enforcer.commitLock.Unlock() + + tx.mutex.Lock() + defer tx.mutex.Unlock() + + if tx.committed { + return errors.New("transaction already committed") + } + if tx.rolledBack { + return errors.New("transaction already rolled back") + } + + // First check if model version has changed. + currentVersion := atomic.LoadInt64(&tx.enforcer.modelVersion) + if currentVersion != tx.baseVersion { + // Model has been modified, need to check for conflicts. + detector := NewConflictDetector( + tx.buffer.GetModelSnapshot(), + tx.enforcer.model, + tx.buffer.GetOperations(), + ) + if err := detector.DetectConflicts(); err != nil { + _ = tx.txContext.Rollback() + tx.enforcer.activeTransactions.Delete(tx.id) + return err + } + } + + // If no operations, just commit the database transaction and clear state. + if !tx.buffer.HasOperations() { + if err := tx.txContext.Commit(); err != nil { + return err + } + tx.committed = true + tx.enforcer.activeTransactions.Delete(tx.id) + return nil + } + + // Phase 1: Apply all buffered operations to the database + if err := tx.applyOperationsToDatabase(); err != nil { + // Rollback database transaction on failure. + _ = tx.txContext.Rollback() + tx.enforcer.activeTransactions.Delete(tx.id) + return err + } + + // Commit database transaction. + if err := tx.txContext.Commit(); err != nil { + tx.enforcer.activeTransactions.Delete(tx.id) + return err + } + + // Phase 2: Apply changes to the in-memory model + if err := tx.applyOperationsToModel(); err != nil { + // At this point, database is committed but model update failed. + // This is a critical error that should not happen in normal circumstances. + tx.enforcer.activeTransactions.Delete(tx.id) + return errors.New("critical error: database committed but model update failed: " + err.Error()) + } + + // Increment model version number. + atomic.AddInt64(&tx.enforcer.modelVersion, 1) + + tx.committed = true + tx.enforcer.activeTransactions.Delete(tx.id) + + return nil +} + +// Rollback rolls back the transaction. +// This will rollback the database transaction and clear the transaction state. +func (tx *Transaction) Rollback() error { + // Try to acquire the commit lock with timeout. + if !tryLockWithTimeout(&tx.enforcer.commitLock, tx.startTime, defaultLockTimeout) { + tx.enforcer.activeTransactions.Delete(tx.id) + return errors.New("transaction timeout: failed to acquire lock for rollback") + } + defer tx.enforcer.commitLock.Unlock() + + tx.mutex.Lock() + defer tx.mutex.Unlock() + + if tx.committed { + return errors.New("transaction already committed") + } + if tx.rolledBack { + return errors.New("transaction already rolled back") + } + + // Rollback database transaction. + if err := tx.txContext.Rollback(); err != nil { + return err + } + + tx.rolledBack = true + tx.enforcer.activeTransactions.Delete(tx.id) + + return nil +} + +// applyOperationsToDatabase applies all buffered operations to the database. +func (tx *Transaction) applyOperationsToDatabase() error { + operations := tx.buffer.GetOperations() + txAdapter := tx.txContext.GetAdapter() + + for _, op := range operations { + switch op.Type { + case persist.OperationAdd: + if err := tx.applyAddOperationToDatabase(txAdapter, op); err != nil { + return err + } + case persist.OperationRemove: + if err := tx.applyRemoveOperationToDatabase(txAdapter, op); err != nil { + return err + } + case persist.OperationUpdate: + if err := tx.applyUpdateOperationToDatabase(txAdapter, op); err != nil { + return err + } + } + } + + return nil +} + +// applyAddOperationToDatabase applies an add operation to the database. +func (tx *Transaction) applyAddOperationToDatabase(adapter persist.Adapter, op persist.PolicyOperation) error { + if batchAdapter, ok := adapter.(persist.BatchAdapter); ok { + // Use batch operation if available. + return batchAdapter.AddPolicies(op.Section, op.PolicyType, op.Rules) + } else { + // Fall back to individual operations. + for _, rule := range op.Rules { + if err := adapter.AddPolicy(op.Section, op.PolicyType, rule); err != nil { + return err + } + } + } + return nil +} + +// applyRemoveOperationToDatabase applies a remove operation to the database. +func (tx *Transaction) applyRemoveOperationToDatabase(adapter persist.Adapter, op persist.PolicyOperation) error { + if batchAdapter, ok := adapter.(persist.BatchAdapter); ok { + // Use batch operation if available. + return batchAdapter.RemovePolicies(op.Section, op.PolicyType, op.Rules) + } else { + // Fall back to individual operations. + for _, rule := range op.Rules { + if err := adapter.RemovePolicy(op.Section, op.PolicyType, rule); err != nil { + return err + } + } + } + return nil +} + +// applyUpdateOperationToDatabase applies an update operation to the database. +func (tx *Transaction) applyUpdateOperationToDatabase(adapter persist.Adapter, op persist.PolicyOperation) error { + if updateAdapter, ok := adapter.(persist.UpdatableAdapter); ok { + // Use update operation if available. + return updateAdapter.UpdatePolicies(op.Section, op.PolicyType, op.OldRules, op.Rules) + } + + // Fall back to remove + add. + for i, oldRule := range op.OldRules { + if err := adapter.RemovePolicy(op.Section, op.PolicyType, oldRule); err != nil { + return err + } + if err := adapter.AddPolicy(op.Section, op.PolicyType, op.Rules[i]); err != nil { + return err + } + } + return nil +} + +// applyOperationsToModel applies all buffered operations to the in-memory model. +func (tx *Transaction) applyOperationsToModel() error { + // Create new model with all operations applied. + newModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot()) + if err != nil { + return err + } + + // Replace the enforcer's model. + tx.enforcer.model = newModel + tx.enforcer.invalidateMatcherMap() + + // Rebuild role links if necessary. + if tx.enforcer.autoBuildRoleLinks { + // Check if any operations involved grouping policies. + operations := tx.buffer.GetOperations() + needRoleRebuild := false + + for _, op := range operations { + if op.Section == "g" { + needRoleRebuild = true + break + } + } + + if needRoleRebuild { + if err := tx.enforcer.BuildRoleLinks(); err != nil { + return err + } + } + } + + return nil +} + +// IsCommitted returns true if the transaction has been committed. +func (tx *Transaction) IsCommitted() bool { + tx.mutex.RLock() + defer tx.mutex.RUnlock() + return tx.committed +} + +// IsRolledBack returns true if the transaction has been rolled back. +func (tx *Transaction) IsRolledBack() bool { + tx.mutex.RLock() + defer tx.mutex.RUnlock() + return tx.rolledBack +} + +// IsActive returns true if the transaction is still active (not committed or rolled back). +func (tx *Transaction) IsActive() bool { + tx.mutex.RLock() + defer tx.mutex.RUnlock() + return !tx.committed && !tx.rolledBack +} diff --git a/transaction_conflict.go b/transaction_conflict.go new file mode 100644 index 0000000..332203e --- /dev/null +++ b/transaction_conflict.go @@ -0,0 +1,137 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "fmt" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// ConflictError represents a transaction conflict error. +type ConflictError struct { + Operation persist.PolicyOperation + Reason string +} + +func (e *ConflictError) Error() string { + return fmt.Sprintf("transaction conflict: %s for operation %v", e.Reason, e.Operation) +} + +// ConflictDetector detects conflicts between transaction operations and current model state. +type ConflictDetector struct { + baseModel model.Model // Model snapshot when transaction started + currentModel model.Model // Current model state + operations []persist.PolicyOperation // Operations to be applied +} + +// NewConflictDetector creates a new conflict detector instance. +func NewConflictDetector(baseModel, currentModel model.Model, operations []persist.PolicyOperation) *ConflictDetector { + return &ConflictDetector{ + baseModel: baseModel, + currentModel: currentModel, + operations: operations, + } +} + +// DetectConflicts checks for conflicts between the transaction operations and current model state. +// Returns nil if no conflicts are found, otherwise returns a ConflictError describing the conflict. +func (cd *ConflictDetector) DetectConflicts() error { + for _, op := range cd.operations { + var err error + switch op.Type { + case persist.OperationAdd: + // Add operations never conflict + continue + + case persist.OperationRemove: + err = cd.detectRemoveConflict(op) + + case persist.OperationUpdate: + err = cd.detectUpdateConflict(op) + } + + if err != nil { + return err + } + } + return nil +} + +// detectRemoveConflict checks for conflicts in remove operations. +func (cd *ConflictDetector) detectRemoveConflict(op persist.PolicyOperation) error { + for _, rule := range op.Rules { + // Check if policy existed in base model + baseHasPolicy, err := cd.baseModel.HasPolicy(op.Section, op.PolicyType, rule) + if err != nil { + return err + } + if !baseHasPolicy { + continue // Policy didn't exist when transaction started + } + + // Check if policy still exists in current model + currentHasPolicy, err := cd.currentModel.HasPolicy(op.Section, op.PolicyType, rule) + if err != nil { + return err + } + if !currentHasPolicy { + return &ConflictError{ + Operation: op, + Reason: "policy has been removed by another transaction", + } + } + } + return nil +} + +// detectUpdateConflict checks for conflicts in update operations. +func (cd *ConflictDetector) detectUpdateConflict(op persist.PolicyOperation) error { + for i, oldRule := range op.OldRules { + if i >= len(op.Rules) { + break + } + newRule := op.Rules[i] + + // Check if old policy still exists + oldExists, err := cd.currentModel.HasPolicy(op.Section, op.PolicyType, oldRule) + if err != nil { + return err + } + if !oldExists { + return &ConflictError{ + Operation: op, + Reason: "policy to be updated no longer exists", + } + } + + // Check if new policy already exists + newExists, err := cd.currentModel.HasPolicy(op.Section, op.PolicyType, newRule) + if err != nil { + return err + } + if newExists { + return &ConflictError{ + Operation: op, + Reason: "target policy already exists", + } + } + } + return nil +} diff --git a/transaction_test.go b/transaction_test.go new file mode 100644 index 0000000..466d65c --- /dev/null +++ b/transaction_test.go @@ -0,0 +1,578 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "context" + "errors" + "testing" + + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// MockTransactionalAdapter implements TransactionalAdapter interface for testing. +type MockTransactionalAdapter struct { + Enforcer *Enforcer +} + +// MockTransactionContext implements TransactionContext interface for testing. +type MockTransactionContext struct { + adapter *MockTransactionalAdapter + committed bool + rolledBack bool +} + +// NewMockTransactionalAdapter creates a new mock adapter. +func NewMockTransactionalAdapter() *MockTransactionalAdapter { + return &MockTransactionalAdapter{} +} + +// LoadPolicy implements Adapter interface. +func (a *MockTransactionalAdapter) LoadPolicy(model model.Model) error { + return nil +} + +// SavePolicy implements Adapter interface. +func (a *MockTransactionalAdapter) SavePolicy(model model.Model) error { + return nil +} + +// AddPolicy implements Adapter interface. +func (a *MockTransactionalAdapter) AddPolicy(sec string, ptype string, rule []string) error { + return nil +} + +// RemovePolicy implements Adapter interface. +func (a *MockTransactionalAdapter) RemovePolicy(sec string, ptype string, rule []string) error { + return nil +} + +// RemoveFilteredPolicy implements Adapter interface. +func (a *MockTransactionalAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { + return nil +} + +// BeginTransaction implements TransactionalAdapter interface. +func (a *MockTransactionalAdapter) BeginTransaction(ctx context.Context) (persist.TransactionContext, error) { + return &MockTransactionContext{adapter: a}, nil +} + +// Commit implements TransactionContext interface. +func (tx *MockTransactionContext) Commit() error { + if tx.committed || tx.rolledBack { + return errors.New("transaction already finished") + } + tx.committed = true + return nil +} + +// Rollback implements TransactionContext interface. +func (tx *MockTransactionContext) Rollback() error { + if tx.committed || tx.rolledBack { + return errors.New("transaction already finished") + } + tx.rolledBack = true + return nil +} + +// GetAdapter implements TransactionContext interface. +func (tx *MockTransactionContext) GetAdapter() persist.Adapter { + return tx.adapter +} + +// Test basic transaction functionality. +func TestTransactionBasicOperations(t *testing.T) { + adapter := NewMockTransactionalAdapter() + e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter) + if err != nil { + t.Fatalf("Failed to create transactional enforcer: %v", err) + } + adapter.Enforcer = e.Enforcer + + ctx := context.Background() + + // Begin transaction. + tx, err := e.BeginTransaction(ctx) + if err != nil { + t.Fatalf("Failed to begin transaction: %v", err) + } + + // Add policies in transaction. + ok, err := tx.AddPolicy("alice", "data1", "read") + if !ok || err != nil { + t.Fatalf("Failed to add policy in transaction: %v", err) + } + + ok, err = tx.AddPolicy("bob", "data2", "write") + if !ok || err != nil { + t.Fatalf("Failed to add policy in transaction: %v", err) + } + + // Commit transaction. + if err := tx.Commit(); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + // Verify transaction was committed. + if !tx.IsCommitted() { + t.Error("Transaction should be committed") + } +} + +// Test transaction rollback. +func TestTransactionRollback(t *testing.T) { + adapter := NewMockTransactionalAdapter() + e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter) + if err != nil { + t.Fatalf("Failed to create transactional enforcer: %v", err) + } + adapter.Enforcer = e.Enforcer + + ctx := context.Background() + + // Begin transaction. + tx, err := e.BeginTransaction(ctx) + if err != nil { + t.Fatalf("Failed to begin transaction: %v", err) + } + + // Add policy in transaction. + ok, err := tx.AddPolicy("alice", "data1", "read") + if !ok || err != nil { + t.Fatalf("Failed to add policy in transaction: %v", err) + } + + // Rollback transaction. + if err := tx.Rollback(); err != nil { + t.Fatalf("Failed to rollback transaction: %v", err) + } + + // Verify transaction was rolled back. + if !tx.IsRolledBack() { + t.Error("Transaction should be rolled back") + } +} + +// Test concurrent transactions. +func TestConcurrentTransactions(t *testing.T) { + adapter := NewMockTransactionalAdapter() + e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter) + if err != nil { + t.Fatalf("Failed to create transactional enforcer: %v", err) + } + adapter.Enforcer = e.Enforcer + + ctx := context.Background() + + // Start first transaction + tx1, err := e.BeginTransaction(ctx) + if err != nil { + t.Fatalf("Failed to begin transaction 1: %v", err) + } + + // Add policy in first transaction + ok, err := tx1.AddPolicy("alice", "data1", "read") + if !ok || err != nil { + t.Fatalf("Failed to add policy in transaction 1: %v", err) + } + + // Start second transaction + tx2, err := e.BeginTransaction(ctx) + if err != nil { + t.Fatalf("Failed to begin transaction 2: %v", err) + } + + // Add different policy in second transaction + ok, err = tx2.AddPolicy("bob", "data2", "write") + if !ok || err != nil { + t.Fatalf("Failed to add policy in transaction 2: %v", err) + } + + // Commit first transaction + if err := tx1.Commit(); err != nil { + t.Fatalf("Failed to commit transaction 1: %v", err) + } + + // Commit second transaction + if err := tx2.Commit(); err != nil { + t.Fatalf("Failed to commit transaction 2: %v", err) + } + + // Verify transactions were committed + if !tx1.IsCommitted() { + t.Error("Transaction 1 should be committed") + } + if !tx2.IsCommitted() { + t.Error("Transaction 2 should be committed") + } +} + +// Test transaction conflicts. +func TestTransactionConflicts(t *testing.T) { + adapter := NewMockTransactionalAdapter() + e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter) + if err != nil { + t.Fatalf("Failed to create transactional enforcer: %v", err) + } + adapter.Enforcer = e.Enforcer + + ctx := context.Background() + + // Test Case 1: Two transactions commit + t.Run("TwoTransactionsCommit", func(t *testing.T) { + tx1, _ := e.BeginTransaction(ctx) + tx2, _ := e.BeginTransaction(ctx) + + // Commit both transactions + if err := tx1.Commit(); err != nil { + t.Fatalf("Failed to commit tx1: %v", err) + } + if err := tx2.Commit(); err != nil { + t.Fatalf("Failed to commit tx2: %v", err) + } + + // Verify both transactions were committed + if !tx1.IsCommitted() { + t.Error("Transaction 1 should be committed") + } + if !tx2.IsCommitted() { + t.Error("Transaction 2 should be committed") + } + }) + + // Test Case 2: Transaction rollback + t.Run("TransactionRollback", func(t *testing.T) { + tx, _ := e.BeginTransaction(ctx) + + // Rollback transaction + if err := tx.Rollback(); err != nil { + t.Fatalf("Failed to rollback transaction: %v", err) + } + + // Verify transaction was rolled back + if !tx.IsRolledBack() { + t.Error("Transaction should be rolled back") + } + }) + + // Test Case 3: Cannot commit after rollback + t.Run("NoCommitAfterRollback", func(t *testing.T) { + tx, _ := e.BeginTransaction(ctx) + + // Rollback transaction + if err := tx.Rollback(); err != nil { + t.Fatalf("Failed to rollback transaction: %v", err) + } + + // Try to commit + if err := tx.Commit(); err == nil { + t.Error("Should not be able to commit after rollback") + } + }) +} + +// Test transaction buffer operations. +func TestTransactionBuffer(t *testing.T) { + adapter := NewMockTransactionalAdapter() + e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter) + if err != nil { + t.Fatalf("Failed to create transactional enforcer: %v", err) + } + adapter.Enforcer = e.Enforcer + + ctx := context.Background() + + tx, err := e.BeginTransaction(ctx) + if err != nil { + t.Fatalf("Failed to begin transaction: %v", err) + } + + // Initially no operations. + if tx.HasOperations() { + t.Fatal("Transaction should have no operations initially") + } + + if tx.OperationCount() != 0 { + t.Fatal("Operation count should be 0 initially") + } + + // Add some operations. + tx.AddPolicy("alice", "data1", "read") + tx.AddPolicy("bob", "data2", "write") + + if !tx.HasOperations() { + t.Fatal("Transaction should have operations") + } + + if tx.OperationCount() != 2 { + t.Fatalf("Expected 2 operations, got %d", tx.OperationCount()) + } + + // Get buffered model. + bufferedModel, err := tx.GetBufferedModel() + if err != nil { + t.Fatalf("Failed to get buffered model: %v", err) + } + + // Check that buffered model contains the policies. + hasPolicy, _ := bufferedModel.HasPolicy("p", "p", []string{"alice", "data1", "read"}) + if !hasPolicy { + t.Fatal("Buffered model should contain the added policy") + } + + tx.Rollback() +} + +// newTestTransactionalEnforcer creates a transactional enforcer backed by the mock adapter. +func newTestTransactionalEnforcer(t *testing.T) *TransactionalEnforcer { + t.Helper() + + adapter := NewMockTransactionalAdapter() + e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter) + if err != nil { + t.Fatalf("Failed to create transactional enforcer: %v", err) + } + adapter.Enforcer = e.Enforcer + + return e +} + +// hasPolicy reports whether the enforcer model holds the given policy rule. +func hasPolicy(t *testing.T, e *TransactionalEnforcer, params ...interface{}) bool { + t.Helper() + + ok, err := e.HasPolicy(params...) + if err != nil { + t.Fatalf("Failed to check policy %v: %v", params, err) + } + return ok +} + +// hasGroupingPolicy reports whether the enforcer model holds the given grouping policy rule. +func hasGroupingPolicy(t *testing.T, e *TransactionalEnforcer, params ...interface{}) bool { + t.Helper() + + ok, err := e.HasGroupingPolicy(params...) + if err != nil { + t.Fatalf("Failed to check grouping policy %v: %v", params, err) + } + return ok +} + +// Test batch grouping policy operations within a transaction. +func TestTransactionGroupingPolicies(t *testing.T) { + e := newTestTransactionalEnforcer(t) + ctx := context.Background() + + err := e.WithTransaction(ctx, func(tx *Transaction) error { + if ok, err := tx.AddPolicy("admin", "data1", "read"); !ok || err != nil { + t.Fatalf("Failed to add policy in transaction: %v", err) + } + if ok, err := tx.AddGroupingPolicies([][]string{{"alice", "admin"}, {"bob", "admin"}}); !ok || err != nil { + t.Fatalf("Failed to add grouping policies in transaction: %v", err) + } + return nil + }) + if err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + if !hasGroupingPolicy(t, e, "alice", "admin") || !hasGroupingPolicy(t, e, "bob", "admin") { + t.Fatal("Both grouping policies should have been added") + } + + // Role links must have been rebuilt for the committed grouping policies. + if allowed, enforceErr := e.Enforce("alice", "data1", "read"); enforceErr != nil || !allowed { + t.Fatalf("alice should inherit the admin permission, got %v (err %v)", allowed, enforceErr) + } + + // Removing them again must take effect too. + err = e.WithTransaction(ctx, func(tx *Transaction) error { + removed, removeErr := tx.RemoveGroupingPolicies([][]string{{"alice", "admin"}, {"bob", "admin"}}) + if !removed || removeErr != nil { + t.Fatalf("Failed to remove grouping policies in transaction: %v", removeErr) + } + return nil + }) + if err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + if hasGroupingPolicy(t, e, "alice", "admin") || hasGroupingPolicy(t, e, "bob", "admin") { + t.Fatal("Both grouping policies should have been removed") + } + if ok, _ := e.Enforce("alice", "data1", "read"); ok { + t.Fatal("alice should no longer inherit the admin permission") + } +} + +// Test updating a grouping policy within a transaction. +func TestTransactionUpdateGroupingPolicy(t *testing.T) { + e := newTestTransactionalEnforcer(t) + ctx := context.Background() + + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + _, err := tx.AddGroupingPolicy("alice", "admin") + return err + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + // Updating a rule that does not exist changes nothing. + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + ok, err := tx.UpdateGroupingPolicy([]string{"carol", "admin"}, []string{"carol", "user"}) + if err != nil { + return err + } + if ok { + t.Fatal("Updating a missing grouping policy should return false") + } + return nil + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + // Mismatched batch lengths are rejected. + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + if _, err := tx.UpdateGroupingPolicies([][]string{{"alice", "admin"}}, [][]string{}); err == nil { + t.Fatal("Mismatched old/new rule counts should return an error") + } + return nil + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + ok, err := tx.UpdateGroupingPolicy([]string{"alice", "admin"}, []string{"alice", "user"}) + if !ok || err != nil { + t.Fatalf("Failed to update grouping policy in transaction: %v", err) + } + return nil + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + if hasGroupingPolicy(t, e, "alice", "admin") { + t.Fatal("The old grouping policy should have been removed") + } + if !hasGroupingPolicy(t, e, "alice", "user") { + t.Fatal("The new grouping policy should have been added") + } +} + +// Test filtered removal within a transaction. +func TestTransactionRemoveFilteredPolicy(t *testing.T) { + e := newTestTransactionalEnforcer(t) + ctx := context.Background() + + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + if _, err := tx.AddPolicies([][]string{ + {"alice", "data1", "read"}, + {"alice", "data2", "write"}, + {"bob", "data2", "write"}, + }); err != nil { + return err + } + _, err := tx.AddGroupingPolicies([][]string{{"alice", "admin"}, {"bob", "admin"}}) + return err + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + // A filter that matches nothing is a no-op. + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + ok, err := tx.RemoveFilteredPolicy(0, "carol") + if err != nil { + return err + } + if ok || tx.HasOperations() { + t.Fatal("A filter matching no rule should buffer nothing") + } + return nil + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + ok, err := tx.RemoveFilteredPolicy(0, "alice") + if !ok || err != nil { + t.Fatalf("Failed to remove filtered policy in transaction: %v", err) + } + + // The removal is visible inside the transaction before committing. + bufferedModel, err := tx.GetBufferedModel() + if err != nil { + return err + } + if has, _ := bufferedModel.HasPolicy("p", "p", []string{"alice", "data1", "read"}); has { + t.Fatal("The buffered model should no longer contain alice's policy") + } + + // But not outside of it. + if !hasPolicy(t, e, "alice", "data1", "read") { + t.Fatal("The enforcer model should still contain alice's policy before commit") + } + + ok, err = tx.RemoveFilteredGroupingPolicy(1, "admin") + if !ok || err != nil { + t.Fatalf("Failed to remove filtered grouping policy in transaction: %v", err) + } + return nil + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + if hasPolicy(t, e, "alice", "data1", "read") || hasPolicy(t, e, "alice", "data2", "write") { + t.Fatal("All of alice's policies should have been removed") + } + if !hasPolicy(t, e, "bob", "data2", "write") { + t.Fatal("bob's policy should have been kept") + } + if hasGroupingPolicy(t, e, "alice", "admin") || hasGroupingPolicy(t, e, "bob", "admin") { + t.Fatal("All grouping policies for admin should have been removed") + } +} + +// Test that a rolled back filtered removal leaves the model untouched. +func TestTransactionRemoveFilteredPolicyRollback(t *testing.T) { + e := newTestTransactionalEnforcer(t) + ctx := context.Background() + + if err := e.WithTransaction(ctx, func(tx *Transaction) error { + _, err := tx.AddPolicy("alice", "data1", "read") + return err + }); err != nil { + t.Fatalf("Failed to commit transaction: %v", err) + } + + wantErr := errors.New("business operation failed") + err := e.WithTransaction(ctx, func(tx *Transaction) error { + if _, err := tx.RemoveFilteredPolicy(0, "alice"); err != nil { + return err + } + return wantErr + }) + if !errors.Is(err, wantErr) { + t.Fatalf("Expected the business error to be returned, got %v", err) + } + + if !hasPolicy(t, e, "alice", "data1", "read") { + t.Fatal("The policy should still exist after a rollback") + } +} diff --git a/util/builtin_operators.go b/util/builtin_operators.go new file mode 100644 index 0000000..cb0361e --- /dev/null +++ b/util/builtin_operators.go @@ -0,0 +1,652 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package util + +import ( + "errors" + "fmt" + "net" + "regexp" + "strings" + "sync" + "time" + + "github.com/bmatcuk/doublestar/v4" + + "github.com/casbin/casbin/v3/rbac" + + "github.com/casbin/govaluate" +) + +var ( + keyMatch2Re = regexp.MustCompile(`:[^/]+`) + keyMatch3Re = regexp.MustCompile(`\{[^/]+\}`) + keyMatch4Re = regexp.MustCompile(`{([^/]+)}`) + keyMatch5Re = regexp.MustCompile(`\{[^/]+\}`) + keyGet2Re1 = regexp.MustCompile(`:[^/]+`) + keyGet3Re1 = regexp.MustCompile(`\{[^/]+?\}`) // non-greedy match of `{...}` to support multiple {} in `/.../` + reCache = map[string]*regexp.Regexp{} + reCacheMu = sync.RWMutex{} +) + +// regexpMetaChars is exactly the set of characters that regexp.QuoteMeta escapes. +// It is spelled out instead of calling QuoteMeta so that keyMatchShortcut never +// allocates; TestRegexpMetaChars guards the two against drifting apart. +const regexpMetaChars = `\.+*?()|[]{}^$` + +// keyMatchShortcut answers KeyMatch2/3/4/5 for the two cases that need no regexp +// work at all. ok reports whether matched is the final answer; when it is false +// the caller falls through to its unchanged rewrite-and-match path. +// +// The two cases are: +// +// 1. key2 == "*", the bare wildcard used as the "all domains" key. The regexp +// path reaches the same answer via "^*$", which only works by accident (see +// issue #330); making it explicit is both faster and less fragile. +// +// 2. key2 is plain, i.e. it holds no regexp metacharacter and none of the +// matcher's own pattern characters (extraChars, ":" for KeyMatch2's :param +// dialect; the "{}" and "*" of the other matchers are already metacharacters). +// Then "^"+key2+"$" can only match key2 itself, so key1 == key2 is the answer. +// +// Case 2 has to stay conservative because RegexMatch embeds key2 unescaped: a +// stray metacharacter makes key2 behave as a regexp, so "acme.com" does match +// "acmeXcom", and key1 == key2 is not sufficient either ("^a+b$" does not match +// "a+b"). Any such key2 keeps taking the regexp path. +// +// This matters because a DomainMatchingFunc is called O(N*D) times while building +// role links, and real deployments mostly hold concrete domains ("team/123") that +// pay for the full pipeline only to come back false. +func keyMatchShortcut(key1 string, key2 string, extraChars string) (matched bool, ok bool) { + if key2 == "*" { + return true, true + } + + if !strings.ContainsAny(key2, regexpMetaChars) && !strings.ContainsAny(key2, extraChars) { + return key1 == key2, true + } + + return false, false +} + +// compiledPattern is the fully prepared form of a matcher's key2: the regexp that +// key1 is tested against, plus the names of the pattern variables in the order +// their capture groups appear (only KeyGet2/KeyGet3/KeyMatch4 use tokens). +// +// Everything in it is a pure function of key2, so it is built once per distinct +// key2 and reused afterwards. +type compiledPattern struct { + re *regexp.Regexp + tokens []string +} + +// patternCache memoizes compiledPattern by raw key2, one cache per matcher since +// each of them reads key2 in its own dialect. Only compiling the regexp used to be +// cached (see mustCompileOrGet), which still left every call rebuilding the regexp +// source with a chain of ReplaceAll passes just to look it up. Keying on key2 +// instead makes the steady state a single map read. +// +// A policy holds a bounded set of patterns, so the cache converges to that set +// after the first pass; sync.Map suits that read-mostly shape. +type patternCache struct { + m sync.Map // string -> *compiledPattern +} + +func (c *patternCache) get(key2 string, build func(key2 string) *compiledPattern) *compiledPattern { + if v, ok := c.m.Load(key2); ok { + return v.(*compiledPattern) + } + + p := build(key2) + c.m.Store(key2, p) + + return p +} + +var ( + keyMatch2Cache patternCache + keyMatch3Cache patternCache + keyMatch4Cache patternCache + keyMatch5Cache patternCache + keyGet2Cache patternCache + keyGet3Cache patternCache +) + +func mustCompileOrGet(key string) *regexp.Regexp { + reCacheMu.RLock() + re, ok := reCache[key] + reCacheMu.RUnlock() + + if !ok { + re = regexp.MustCompile(key) + reCacheMu.Lock() + reCache[key] = re + reCacheMu.Unlock() + } + + return re +} + +// validate the variadic parameter size and type as string. +func validateVariadicArgs(expectedLen int, args ...interface{}) error { + if len(args) != expectedLen { + return fmt.Errorf("expected %d arguments, but got %d", expectedLen, len(args)) + } + + for _, p := range args { + _, ok := p.(string) + if !ok { + return errors.New("argument must be a string") + } + } + + return nil +} + +// validate the variadic string parameter size. +func validateVariadicStringArgs(expectedLen int, args ...string) error { + if len(args) != expectedLen { + return fmt.Errorf("expected %d arguments, but got %d", expectedLen, len(args)) + } + return nil +} + +// KeyMatch determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. +// For example, "/foo/bar" matches "/foo/*". +func KeyMatch(key1 string, key2 string) bool { + i := strings.Index(key2, "*") + if i == -1 { + return key1 == key2 + } + + if len(key1) > i { + return key1[:i] == key2[:i] + } + return key1 == key2[:i] +} + +// KeyMatchFunc is the wrapper for KeyMatch. +func KeyMatchFunc(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyMatch", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return KeyMatch(name1, name2), nil +} + +// KeyGet returns the matched part +// For example, "/foo/bar/foo" matches "/foo/*" +// "bar/foo" will been returned. +func KeyGet(key1, key2 string) string { + i := strings.Index(key2, "*") + if i == -1 { + return "" + } + if len(key1) > i { + if key1[:i] == key2[:i] { + return key1[i:] + } + } + return "" +} + +// KeyGetFunc is the wrapper for KeyGet. +func KeyGetFunc(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyGet", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return KeyGet(name1, name2), nil +} + +// KeyMatch2 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. +// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource". +func KeyMatch2(key1 string, key2 string) bool { + if matched, ok := keyMatchShortcut(key1, key2, ":"); ok { + return matched + } + + return keyMatch2Cache.get(key2, buildKeyMatch2).re.MatchString(key1) +} + +func buildKeyMatch2(key2 string) *compiledPattern { + key2 = strings.ReplaceAll(key2, "/*", "/.*") + + key2 = keyMatch2Re.ReplaceAllString(key2, "$1[^/]+$2") + + return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$")} +} + +// KeyMatch2Func is the wrapper for KeyMatch2. +func KeyMatch2Func(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyMatch2", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return KeyMatch2(name1, name2), nil +} + +// KeyGet2 returns value matched pattern +// For example, "/resource1" matches "/:resource" +// if the pathVar == "resource", then "resource1" will be returned. +func KeyGet2(key1, key2 string, pathVar string) string { + p := keyGet2Cache.get(key2, buildKeyGet2) + + values := p.re.FindAllStringSubmatch(key1, -1) + if len(values) == 0 { + return "" + } + for i, key := range p.tokens { + if pathVar == key { + return values[0][i+1] + } + } + return "" +} + +func buildKeyGet2(key2 string) *compiledPattern { + key2 = strings.ReplaceAll(key2, "/*", "/.*") + + // The ":" is dropped here so that the lookup in KeyGet2 is a plain comparison. + tokens := keyGet2Re1.FindAllString(key2, -1) + for i, token := range tokens { + tokens[i] = token[1:] + } + + key2 = keyGet2Re1.ReplaceAllString(key2, "$1([^/]+)$2") + + return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$"), tokens: tokens} +} + +// KeyGet2Func is the wrapper for KeyGet2. +func KeyGet2Func(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(3, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyGet2", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + key := args[2].(string) + + return KeyGet2(name1, name2, key), nil +} + +// KeyMatch3 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. +// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/{resource}". +func KeyMatch3(key1 string, key2 string) bool { + if matched, ok := keyMatchShortcut(key1, key2, ""); ok { + return matched + } + + return keyMatch3Cache.get(key2, buildKeyMatch3).re.MatchString(key1) +} + +func buildKeyMatch3(key2 string) *compiledPattern { + key2 = strings.ReplaceAll(key2, "/*", "/.*") + key2 = keyMatch3Re.ReplaceAllString(key2, "$1[^/]+$2") + + return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$")} +} + +// KeyMatch3Func is the wrapper for KeyMatch3. +func KeyMatch3Func(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyMatch3", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return KeyMatch3(name1, name2), nil +} + +// KeyGet3 returns value matched pattern +// For example, "project/proj_project1_admin/" matches "project/proj_{project}_admin/" +// if the pathVar == "project", then "project1" will be returned. +func KeyGet3(key1, key2 string, pathVar string) string { + p := keyGet3Cache.get(key2, buildKeyGet3) + + values := p.re.FindAllStringSubmatch(key1, -1) + if len(values) == 0 { + return "" + } + for i, key := range p.tokens { + if pathVar == key { + return values[0][i+1] + } + } + return "" +} + +func buildKeyGet3(key2 string) *compiledPattern { + key2 = strings.ReplaceAll(key2, "/*", "/.*") + + // The surrounding "{}" is dropped here so that the lookup in KeyGet3 is a plain + // comparison. + tokens := keyGet3Re1.FindAllString(key2, -1) + for i, token := range tokens { + tokens[i] = token[1 : len(token)-1] + } + + key2 = keyGet3Re1.ReplaceAllString(key2, "$1([^/]+?)$2") + + return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$"), tokens: tokens} +} + +// KeyGet3Func is the wrapper for KeyGet3. +func KeyGet3Func(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(3, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyGet3", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + key := args[2].(string) + + return KeyGet3(name1, name2, key), nil +} + +// KeyMatch4 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. +// Besides what KeyMatch3 does, KeyMatch4 can also match repeated patterns: +// "/parent/123/child/123" matches "/parent/{id}/child/{id}" +// "/parent/123/child/456" does not match "/parent/{id}/child/{id}" +// But KeyMatch3 will match both. +func KeyMatch4(key1 string, key2 string) bool { + if matched, ok := keyMatchShortcut(key1, key2, ""); ok { + return matched + } + + p := keyMatch4Cache.get(key2, buildKeyMatch4) + + matches := p.re.FindStringSubmatch(key1) + if matches == nil { + return false + } + matches = matches[1:] + + if len(p.tokens) != len(matches) { + panic(errors.New("KeyMatch4: number of tokens is not equal to number of values")) + } + + values := map[string]string{} + + for key, token := range p.tokens { + if _, ok := values[token]; !ok { + values[token] = matches[key] + } + if values[token] != matches[key] { + return false + } + } + + return true +} + +func buildKeyMatch4(key2 string) *compiledPattern { + key2 = strings.ReplaceAll(key2, "/*", "/.*") + + tokens := []string{} + + key2 = keyMatch4Re.ReplaceAllStringFunc(key2, func(s string) string { + tokens = append(tokens, s[1:len(s)-1]) + return "([^/]+)" + }) + + return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$"), tokens: tokens} +} + +// KeyMatch4Func is the wrapper for KeyMatch4. +func KeyMatch4Func(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyMatch4", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return KeyMatch4(name1, name2), nil +} + +// KeyMatch5 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a * +// For example, +// - "/foo/bar?status=1&type=2" matches "/foo/bar" +// - "/parent/child1" and "/parent/child1" matches "/parent/*" +// - "/parent/child1?status=1" matches "/parent/*". +func KeyMatch5(key1 string, key2 string) bool { + i := strings.Index(key1, "?") + + if i != -1 { + key1 = key1[:i] + } + + // After the query string is stripped, so that key1 is compared in the same + // shape the regexp path would have matched. + if matched, ok := keyMatchShortcut(key1, key2, ""); ok { + return matched + } + + return keyMatch5Cache.get(key2, buildKeyMatch5).re.MatchString(key1) +} + +func buildKeyMatch5(key2 string) *compiledPattern { + key2 = strings.ReplaceAll(key2, "/*", "/.*") + key2 = keyMatch5Re.ReplaceAllString(key2, "$1[^/]+$2") + + return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$")} +} + +// KeyMatch5Func is the wrapper for KeyMatch5. +func KeyMatch5Func(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyMatch5", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return KeyMatch5(name1, name2), nil +} + +// RegexMatch determines whether key1 matches the pattern of key2 in regular expression. +func RegexMatch(key1 string, key2 string) bool { + return mustCompileOrGet(key2).MatchString(key1) +} + +// RegexMatchFunc is the wrapper for RegexMatch. +func RegexMatchFunc(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "regexMatch", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return RegexMatch(name1, name2), nil +} + +// IPMatch determines whether IP address ip1 matches the pattern of IP address ip2, ip2 can be an IP address or a CIDR pattern. +// For example, "192.168.2.123" matches "192.168.2.0/24". +func IPMatch(ip1 string, ip2 string) bool { + objIP1 := net.ParseIP(ip1) + if objIP1 == nil { + panic("invalid argument: ip1 in IPMatch() function is not an IP address.") + } + + _, cidr, err := net.ParseCIDR(ip2) + if err != nil { + objIP2 := net.ParseIP(ip2) + if objIP2 == nil { + panic("invalid argument: ip2 in IPMatch() function is neither an IP address nor a CIDR.") + } + + return objIP1.Equal(objIP2) + } + + return cidr.Contains(objIP1) +} + +// IPMatchFunc is the wrapper for IPMatch. +func IPMatchFunc(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "ipMatch", err) + } + + ip1 := args[0].(string) + ip2 := args[1].(string) + + return IPMatch(ip1, ip2), nil +} + +// GlobMatch determines whether key1 matches the pattern of key2 using glob pattern. +func GlobMatch(key1 string, key2 string) (bool, error) { + return doublestar.Match(key2, key1) +} + +// GlobMatchFunc is the wrapper for GlobMatch. +func GlobMatchFunc(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "globMatch", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return GlobMatch(name1, name2) +} + +// GenerateGFunction is the factory method of the g(_, _[, _]) function. +// If useCache is true, results are memoized per unique argument combination for performance. +// Set useCache to false when inputs are high-cardinality (e.g. UUIDs) to avoid unbounded memory growth. +func GenerateGFunction(rm rbac.RoleManager, useCache bool) govaluate.ExpressionFunction { + memorized := sync.Map{} + return func(args ...interface{}) (interface{}, error) { + // Like all our other govaluate functions, all args are strings. + + if useCache { + // Allocate and generate a cache key from the arguments... + total := len(args) + for _, a := range args { + aStr := a.(string) + total += len(aStr) + } + builder := strings.Builder{} + builder.Grow(total) + for _, arg := range args { + builder.WriteByte(0) + builder.WriteString(arg.(string)) + } + key := builder.String() + + // ...and see if we've already calculated this. + if v, found := memorized.Load(key); found { + return v, nil + } + + // If not, do the calculation. + // There are guaranteed to be exactly 2 or 3 arguments. + name1, name2 := args[0].(string), args[1].(string) + var v interface{} + if rm == nil { + v = name1 == name2 + } else if len(args) == 2 { + v, _ = rm.HasLink(name1, name2) + } else { + domain := args[2].(string) + v, _ = rm.HasLink(name1, name2, domain) + } + + memorized.Store(key, v) + return v, nil + } + + // No caching path. + name1, name2 := args[0].(string), args[1].(string) + var v interface{} + if rm == nil { + v = name1 == name2 + } else if len(args) == 2 { + v, _ = rm.HasLink(name1, name2) + } else { + domain := args[2].(string) + v, _ = rm.HasLink(name1, name2, domain) + } + return v, nil + } +} + +// GenerateConditionalGFunction is the factory method of the g(_, _[, _]) function with conditions. +func GenerateConditionalGFunction(crm rbac.ConditionalRoleManager) govaluate.ExpressionFunction { + return func(args ...interface{}) (interface{}, error) { + // Like all our other govaluate functions, all args are strings. + var hasLink bool + + name1, name2 := args[0].(string), args[1].(string) + if crm == nil { + hasLink = name1 == name2 + } else if len(args) == 2 { + hasLink, _ = crm.HasLink(name1, name2) + } else { + domain := args[2].(string) + hasLink, _ = crm.HasLink(name1, name2, domain) + } + + return hasLink, nil + } +} + +// builtin LinkConditionFunc + +// TimeMatchFunc is the wrapper for TimeMatch. +func TimeMatchFunc(args ...string) (bool, error) { + if err := validateVariadicStringArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "TimeMatch", err) + } + return TimeMatch(args[0], args[1]) +} + +// TimeMatch determines whether the current time is between startTime and endTime. +// You can use "_" to indicate that the parameter is ignored. +func TimeMatch(startTime, endTime string) (bool, error) { + now := time.Now() + if startTime != "_" { + if start, err := time.Parse("2006-01-02 15:04:05", startTime); err != nil { + return false, err + } else if !now.After(start) { + return false, nil + } + } + + if endTime != "_" { + if end, err := time.Parse("2006-01-02 15:04:05", endTime); err != nil { + return false, err + } else if !now.Before(end) { + return false, nil + } + } + + return true, nil +} diff --git a/util/builtin_operators_b_test.go b/util/builtin_operators_b_test.go new file mode 100644 index 0000000..f254e3c --- /dev/null +++ b/util/builtin_operators_b_test.go @@ -0,0 +1,65 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package util + +import "testing" + +// The key2 of each benchmark holds a pattern character, so the keyMatchShortcut +// fast path does not apply and the regexp pipeline is what is being measured. + +func BenchmarkKeyMatch2(b *testing.B) { + for i := 0; i < b.N; i++ { + KeyMatch2("/alice_data/resource1", "/:name/resource1") + } +} + +func BenchmarkKeyMatch3(b *testing.B) { + for i := 0; i < b.N; i++ { + KeyMatch3("/proxy/myid/res/1", "/proxy/{id}/res/{rid}") + } +} + +func BenchmarkKeyMatch4(b *testing.B) { + for i := 0; i < b.N; i++ { + KeyMatch4("/parent/123/child/123", "/parent/{id}/child/{id}") + } +} + +func BenchmarkKeyMatch5(b *testing.B) { + for i := 0; i < b.N; i++ { + KeyMatch5("/parent/child1?status=1", "/parent/{id}") + } +} + +func BenchmarkKeyGet2(b *testing.B) { + for i := 0; i < b.N; i++ { + KeyGet2("/alice_data/resource1", "/:name/resource1", "name") + } +} + +func BenchmarkKeyGet3(b *testing.B) { + for i := 0; i < b.N; i++ { + KeyGet3("project/proj_project1_admin/", "project/proj_{project}_admin/", "project") + } +} + +func BenchmarkRegexMatch(b *testing.B) { + for i := 0; i < b.N; i++ { + RegexMatch("/topic/1", "^/topic/[0-9]+$") + } +} diff --git a/util/builtin_operators_test.go b/util/builtin_operators_test.go new file mode 100644 index 0000000..a4990bb --- /dev/null +++ b/util/builtin_operators_test.go @@ -0,0 +1,739 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package util + +import ( + "testing" +) + +func testKeyMatch(t *testing.T, key1 string, key2 string, res bool) { + t.Helper() + myRes := KeyMatch(key1, key2) + t.Logf("%s < %s: %t", key1, key2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res) + } +} + +func TestKeyMatch(t *testing.T) { + testKeyMatch(t, "/foo", "/foo", true) + testKeyMatch(t, "/foo", "/foo*", true) + testKeyMatch(t, "/foo", "/foo/*", false) + testKeyMatch(t, "/foo/bar", "/foo", false) + testKeyMatch(t, "/foo/bar", "/foo*", true) + testKeyMatch(t, "/foo/bar", "/foo/*", true) + testKeyMatch(t, "/foobar", "/foo", false) + testKeyMatch(t, "/foobar", "/foo*", true) + testKeyMatch(t, "/foobar", "/foo/*", false) +} + +func testKeyGet(t *testing.T, key1 string, key2 string, res string) { + t.Helper() + myRes := KeyGet(key1, key2) + t.Logf(`%s < %s: "%s"`, key1, key2, myRes) + + if myRes != res { + t.Errorf(`%s < %s: "%s", supposed to be "%s"`, key1, key2, myRes, res) + } +} + +func TestKeyGet(t *testing.T) { + testKeyGet(t, "/foo", "/foo", "") + testKeyGet(t, "/foo", "/foo*", "") + testKeyGet(t, "/foo", "/foo/*", "") + testKeyGet(t, "/foo/bar", "/foo", "") + testKeyGet(t, "/foo/bar", "/foo*", "/bar") + testKeyGet(t, "/foo/bar", "/foo/*", "bar") + testKeyGet(t, "/foobar", "/foo", "") + testKeyGet(t, "/foobar", "/foo*", "bar") + testKeyGet(t, "/foobar", "/foo/*", "") +} + +func testKeyMatch2(t *testing.T, key1 string, key2 string, res bool) { + t.Helper() + myRes := KeyMatch2(key1, key2) + t.Logf("%s < %s: %t", key1, key2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res) + } +} + +func testGlobMatch(t *testing.T, key1 string, key2 string, res bool) { + t.Helper() + myRes, err := GlobMatch(key1, key2) + if err != nil { + panic(err) + } + t.Logf("%s < %s: %t", key1, key2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res) + } +} + +func TestKeyMatch2(t *testing.T) { + testKeyMatch2(t, "/foo", "/foo", true) + testKeyMatch2(t, "/foo", "/foo*", true) + testKeyMatch2(t, "/foo", "/foo/*", false) + testKeyMatch2(t, "/foo/bar", "/foo", false) + testKeyMatch2(t, "/foo/bar", "/foo*", false) // different with KeyMatch. + testKeyMatch2(t, "/foo/bar", "/foo/*", true) + testKeyMatch2(t, "/foobar", "/foo", false) + testKeyMatch2(t, "/foobar", "/foo*", false) // different with KeyMatch. + testKeyMatch2(t, "/foobar", "/foo/*", false) + + testKeyMatch2(t, "/", "/:resource", false) + testKeyMatch2(t, "/resource1", "/:resource", true) + testKeyMatch2(t, "/myid", "/:id/using/:resId", false) + testKeyMatch2(t, "/myid/using/myresid", "/:id/using/:resId", true) + + testKeyMatch2(t, "/proxy/myid", "/proxy/:id/*", false) + testKeyMatch2(t, "/proxy/myid/", "/proxy/:id/*", true) + testKeyMatch2(t, "/proxy/myid/res", "/proxy/:id/*", true) + testKeyMatch2(t, "/proxy/myid/res/res2", "/proxy/:id/*", true) + testKeyMatch2(t, "/proxy/myid/res/res2/res3", "/proxy/:id/*", true) + testKeyMatch2(t, "/proxy/", "/proxy/:id/*", false) + + testKeyMatch2(t, "/alice", "/:id", true) + testKeyMatch2(t, "/alice/all", "/:id/all", true) + testKeyMatch2(t, "/alice", "/:id/all", false) + testKeyMatch2(t, "/alice/all", "/:id", false) + + testKeyMatch2(t, "/alice/all", "/:/all", false) +} + +func testKeyGet2(t *testing.T, key1 string, key2 string, pathVar string, res string) { + t.Helper() + myRes := KeyGet2(key1, key2, pathVar) + t.Logf(`%s < %s: %s = "%s"`, key1, key2, pathVar, myRes) + + if myRes != res { + t.Errorf(`%s < %s: %s = "%s" supposed to be "%s"`, key1, key2, pathVar, myRes, res) + } +} + +func TestKeyGet2(t *testing.T) { + testKeyGet2(t, "/foo", "/foo", "id", "") + testKeyGet2(t, "/foo", "/foo*", "id", "") + testKeyGet2(t, "/foo", "/foo/*", "id", "") + testKeyGet2(t, "/foo/bar", "/foo", "id", "") + testKeyGet2(t, "/foo/bar", "/foo*", "id", "") + testKeyGet2(t, "/foo/bar", "/foo/*", "id", "") + testKeyGet2(t, "/foobar", "/foo", "id", "") + testKeyGet2(t, "/foobar", "/foo*", "id", "") + testKeyGet2(t, "/foobar", "/foo/*", "id", "") + + testKeyGet2(t, "/", "/:resource", "resource", "") + testKeyGet2(t, "/resource1", "/:resource", "resource", "resource1") + testKeyGet2(t, "/myid", "/:id/using/:resId", "id", "") + testKeyGet2(t, "/myid/using/myresid", "/:id/using/:resId", "id", "myid") + testKeyGet2(t, "/myid/using/myresid", "/:id/using/:resId", "resId", "myresid") + + testKeyGet2(t, "/proxy/myid", "/proxy/:id/*", "id", "") + testKeyGet2(t, "/proxy/myid/", "/proxy/:id/*", "id", "myid") + testKeyGet2(t, "/proxy/myid/res", "/proxy/:id/*", "id", "myid") + testKeyGet2(t, "/proxy/myid/res/res2", "/proxy/:id/*", "id", "myid") + testKeyGet2(t, "/proxy/myid/res/res2/res3", "/proxy/:id/*", "id", "myid") + testKeyGet2(t, "/proxy/myid/res/res2/res3", "/proxy/:id/res/*", "id", "myid") + testKeyGet2(t, "/proxy/", "/proxy/:id/*", "id", "") + + testKeyGet2(t, "/alice", "/:id", "id", "alice") + testKeyGet2(t, "/alice/all", "/:id/all", "id", "alice") + testKeyGet2(t, "/alice", "/:id/all", "id", "") + testKeyGet2(t, "/alice/all", "/:id", "id", "") + + testKeyGet2(t, "/alice/all", "/:/all", "", "") +} + +func testKeyMatch3(t *testing.T, key1 string, key2 string, res bool) { + t.Helper() + myRes := KeyMatch3(key1, key2) + t.Logf("%s < %s: %t", key1, key2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res) + } +} + +func TestKeyMatch3(t *testing.T) { + // keyMatch3() is similar with KeyMatch2(), except using "/proxy/{id}" instead of "/proxy/:id". + testKeyMatch3(t, "/foo", "/foo", true) + testKeyMatch3(t, "/foo", "/foo*", true) + testKeyMatch3(t, "/foo", "/foo/*", false) + testKeyMatch3(t, "/foo/bar", "/foo", false) + testKeyMatch3(t, "/foo/bar", "/foo*", false) + testKeyMatch3(t, "/foo/bar", "/foo/*", true) + testKeyMatch3(t, "/foobar", "/foo", false) + testKeyMatch3(t, "/foobar", "/foo*", false) + testKeyMatch3(t, "/foobar", "/foo/*", false) + + testKeyMatch3(t, "/", "/{resource}", false) + testKeyMatch3(t, "/resource1", "/{resource}", true) + testKeyMatch3(t, "/myid", "/{id}/using/{resId}", false) + testKeyMatch3(t, "/myid/using/myresid", "/{id}/using/{resId}", true) + + testKeyMatch3(t, "/proxy/myid", "/proxy/{id}/*", false) + testKeyMatch3(t, "/proxy/myid/", "/proxy/{id}/*", true) + testKeyMatch3(t, "/proxy/myid/res", "/proxy/{id}/*", true) + testKeyMatch3(t, "/proxy/myid/res/res2", "/proxy/{id}/*", true) + testKeyMatch3(t, "/proxy/myid/res/res2/res3", "/proxy/{id}/*", true) + testKeyMatch3(t, "/proxy/", "/proxy/{id}/*", false) + + testKeyMatch3(t, "/myid/using/myresid", "/{id/using/{resId}", false) +} + +func testKeyGet3(t *testing.T, key1 string, key2 string, pathVar string, res string) { + t.Helper() + myRes := KeyGet3(key1, key2, pathVar) + t.Logf(`%s < %s: %s = "%s"`, key1, key2, pathVar, myRes) + + if myRes != res { + t.Errorf(`%s < %s: %s = "%s" supposed to be "%s"`, key1, key2, pathVar, myRes, res) + } +} + +func TestKeyGet3(t *testing.T) { + // KeyGet3() is similar with KeyGet2(), except using "/proxy/{id}" instead of "/proxy/:id". + testKeyGet3(t, "/foo", "/foo", "id", "") + testKeyGet3(t, "/foo", "/foo*", "id", "") + testKeyGet3(t, "/foo", "/foo/*", "id", "") + testKeyGet3(t, "/foo/bar", "/foo", "id", "") + testKeyGet3(t, "/foo/bar", "/foo*", "id", "") + testKeyGet3(t, "/foo/bar", "/foo/*", "id", "") + testKeyGet3(t, "/foobar", "/foo", "id", "") + testKeyGet3(t, "/foobar", "/foo*", "id", "") + testKeyGet3(t, "/foobar", "/foo/*", "id", "") + + testKeyGet3(t, "/", "/{resource}", "resource", "") + testKeyGet3(t, "/resource1", "/{resource}", "resource", "resource1") + testKeyGet3(t, "/myid", "/{id}/using/{resId}", "id", "") + testKeyGet3(t, "/myid/using/myresid", "/{id}/using/{resId}", "id", "myid") + testKeyGet3(t, "/myid/using/myresid", "/{id}/using/{resId}", "resId", "myresid") + + testKeyGet3(t, "/proxy/myid", "/proxy/{id}/*", "id", "") + testKeyGet3(t, "/proxy/myid/", "/proxy/{id}/*", "id", "myid") + testKeyGet3(t, "/proxy/myid/res", "/proxy/{id}/*", "id", "myid") + testKeyGet3(t, "/proxy/myid/res/res2", "/proxy/{id}/*", "id", "myid") + testKeyGet3(t, "/proxy/myid/res/res2/res3", "/proxy/{id}/*", "id", "myid") + testKeyGet3(t, "/proxy/", "/proxy/{id}/*", "id", "") + + testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{proj}_admin/info", + "proj", "") + testKeyGet3(t, "/{id/using/myresid", "/{id/using/{resId}", "resId", "myresid") + testKeyGet3(t, "/{id/using/myresid/status}", "/{id/using/{resId}/status}", "resId", "myresid") + + testKeyGet3(t, "/proxy/myid/res/res2/res3", "/proxy/{id}/*/{res}", "res", "res3") + testKeyGet3(t, "/api/project1_admin/info", "/api/{proj}_admin/info", "proj", "project1") + testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{g}_{gn}/{proj}_admin/info", + "g", "group1") + testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{g}_{gn}/{proj}_admin/info", + "gn", "group_name") + testKeyGet3(t, "/api/group1_group_name/project1_admin/info", "/api/{g}_{gn}/{proj}_admin/info", + "proj", "project1") +} + +func testKeyMatch4(t *testing.T, key1 string, key2 string, res bool) { + t.Helper() + myRes := KeyMatch4(key1, key2) + t.Logf("%s < %s: %t", key1, key2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res) + } +} + +func TestKeyMatch4(t *testing.T) { + testKeyMatch4(t, "/parent/123/child/123", "/parent/{id}/child/{id}", true) + testKeyMatch4(t, "/parent/123/child/456", "/parent/{id}/child/{id}", false) + + testKeyMatch4(t, "/parent/123/child/123", "/parent/{id}/child/{another_id}", true) + testKeyMatch4(t, "/parent/123/child/456", "/parent/{id}/child/{another_id}", true) + + testKeyMatch4(t, "/parent/123/child/123/book/123", "/parent/{id}/child/{id}/book/{id}", true) + testKeyMatch4(t, "/parent/123/child/123/book/456", "/parent/{id}/child/{id}/book/{id}", false) + testKeyMatch4(t, "/parent/123/child/456/book/123", "/parent/{id}/child/{id}/book/{id}", false) + testKeyMatch4(t, "/parent/123/child/456/book/", "/parent/{id}/child/{id}/book/{id}", false) + testKeyMatch4(t, "/parent/123/child/456", "/parent/{id}/child/{id}/book/{id}", false) + + testKeyMatch4(t, "/parent/123/child/123", "/parent/{i/d}/child/{i/d}", false) +} + +func testRegexMatch(t *testing.T, key1 string, key2 string, res bool) { + t.Helper() + myRes := RegexMatch(key1, key2) + t.Logf("%s < %s: %t", key1, key2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res) + } +} + +func TestRegexMatch(t *testing.T) { + testRegexMatch(t, "/topic/create", "/topic/create", true) + testRegexMatch(t, "/topic/create/123", "/topic/create", true) + testRegexMatch(t, "/topic/delete", "/topic/create", false) + testRegexMatch(t, "/topic/edit", "/topic/edit/[0-9]+", false) + testRegexMatch(t, "/topic/edit/123", "/topic/edit/[0-9]+", true) + testRegexMatch(t, "/topic/edit/abc", "/topic/edit/[0-9]+", false) + testRegexMatch(t, "/foo/delete/123", "/topic/delete/[0-9]+", false) + testRegexMatch(t, "/topic/delete/0", "/topic/delete/[0-9]+", true) + testRegexMatch(t, "/topic/edit/123s", "/topic/delete/[0-9]+", false) +} + +func testIPMatch(t *testing.T, ip1 string, ip2 string, res bool) { + t.Helper() + myRes := IPMatch(ip1, ip2) + t.Logf("%s < %s: %t", ip1, ip2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", ip1, ip2, !res, res) + } +} + +func TestIPMatch(t *testing.T) { + testIPMatch(t, "192.168.2.123", "192.168.2.0/24", true) + testIPMatch(t, "192.168.2.123", "192.168.3.0/24", false) + testIPMatch(t, "192.168.2.123", "192.168.2.0/16", true) + testIPMatch(t, "192.168.2.123", "192.168.2.123", true) + testIPMatch(t, "192.168.2.123", "192.168.2.123/32", true) + testIPMatch(t, "10.0.0.11", "10.0.0.0/8", true) + testIPMatch(t, "11.0.0.123", "10.0.0.0/8", false) +} + +func testRegexMatchFunc(t *testing.T, res bool, err string, args ...interface{}) { + t.Helper() + myRes, myErr := RegexMatchFunc(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyMatchFunc(t *testing.T, res bool, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyMatchFunc(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyGetFunc(t *testing.T, res interface{}, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyGetFunc(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyMatch2Func(t *testing.T, res bool, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyMatch2Func(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyGet2Func(t *testing.T, res interface{}, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyGet2Func(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyMatch3Func(t *testing.T, res bool, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyMatch3Func(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyGet3Func(t *testing.T, res interface{}, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyGet3Func(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyMatch4Func(t *testing.T, res bool, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyMatch4Func(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testKeyMatch5Func(t *testing.T, res bool, err string, args ...interface{}) { + t.Helper() + myRes, myErr := KeyMatch5Func(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func testIPMatchFunc(t *testing.T, res bool, err string, args ...interface{}) { + t.Helper() + myRes, myErr := IPMatchFunc(args...) + myErrStr := "" + + if myErr != nil { + myErrStr = myErr.Error() + } + + if myRes != res || err != myErrStr { + t.Errorf("%v returns %v %v, supposed to be %v %v", args, myRes, myErr, res, err) + } +} + +func TestRegexMatchFunc(t *testing.T) { + testRegexMatchFunc(t, false, "regexMatch: expected 2 arguments, but got 1", "/topic/create") + testRegexMatchFunc(t, false, "regexMatch: expected 2 arguments, but got 3", "/topic/create/123", "/topic/create", "/topic/update") + testRegexMatchFunc(t, false, "regexMatch: argument must be a string", "/topic/create", false) + testRegexMatchFunc(t, true, "", "/topic/create/123", "/topic/create") +} + +func TestKeyMatchFunc(t *testing.T) { + testKeyMatchFunc(t, false, "keyMatch: expected 2 arguments, but got 1", "/foo") + testKeyMatchFunc(t, false, "keyMatch: expected 2 arguments, but got 3", "/foo/create/123", "/foo/*", "/foo/update/123") + testKeyMatchFunc(t, false, "keyMatch: argument must be a string", "/foo", true) + testKeyMatchFunc(t, false, "", "/foo/bar", "/foo") + testKeyMatchFunc(t, true, "", "/foo/bar", "/foo/*") + testKeyMatchFunc(t, true, "", "/foo/bar", "/foo*") +} + +func TestKeyGetFunc(t *testing.T) { + testKeyGetFunc(t, false, "keyGet: expected 2 arguments, but got 1", "/foo") + testKeyGetFunc(t, false, "keyGet: expected 2 arguments, but got 3", "/foo/bar", "/foo/*", "bar") + testKeyGetFunc(t, false, "keyGet: argument must be a string", "/foo/bar", true) + + testKeyGetFunc(t, "", "", "/foo/bar", "/foo") + testKeyGetFunc(t, "bar", "", "/foo/bar", "/foo/*") + testKeyGetFunc(t, "/bar", "", "/foo/bar", "/foo*") +} + +func TestKeyMatch2Func(t *testing.T) { + testKeyMatch2Func(t, false, "keyMatch2: expected 2 arguments, but got 1", "/") + testKeyMatch2Func(t, false, "keyMatch2: expected 2 arguments, but got 3", "/foo/create/123", "/*", "/foo/update/123") + testKeyMatch2Func(t, false, "keyMatch2: argument must be a string", "/foo", true) + + testKeyMatch2Func(t, false, "", "/", "/:resource") + testKeyMatch2Func(t, true, "", "/resource1", "/:resource") + + testKeyMatch2Func(t, true, "", "/foo", "/foo") + testKeyMatch2Func(t, true, "", "/foo", "/foo*") + testKeyMatch2Func(t, false, "", "/foo", "/foo/*") +} + +func TestKeyGet2Func(t *testing.T) { + testKeyGet2Func(t, false, "keyGet2: expected 3 arguments, but got 2", "/myid/using/myresid", "/:id/using/:resId") + testKeyGet2Func(t, false, "keyGet2: expected 3 arguments, but got 4", "/myid/using/myresid", "/:id/using/:resId", "id", "resId") + testKeyGet2Func(t, false, "keyGet2: argument must be a string", "/myid/using/myresid", "/:id/using/:resId", true) + + testKeyGet2Func(t, "myid", "", "/myid/using/myresid", "/:id/using/:resId", "id") + testKeyGet2Func(t, "myresid", "", "/myid/using/myresid", "/:id/using/:resId", "resId") + testKeyGet2Func(t, "", "", "/myid/using/myresid", "/:id/using/:resId", "missing") +} + +func TestKeyMatch3Func(t *testing.T) { + testKeyMatch3Func(t, false, "keyMatch3: expected 2 arguments, but got 1", "/") + testKeyMatch3Func(t, false, "keyMatch3: expected 2 arguments, but got 3", "/foo/create/123", "/*", "/foo/update/123") + testKeyMatch3Func(t, false, "keyMatch3: argument must be a string", "/foo", true) + + testKeyMatch3Func(t, true, "", "/foo", "/foo") + testKeyMatch3Func(t, true, "", "/foo", "/foo*") + testKeyMatch3Func(t, false, "", "/foo", "/foo/*") + testKeyMatch3Func(t, false, "", "/foo/bar", "/foo") + testKeyMatch3Func(t, false, "", "/foo/bar", "/foo*") + testKeyMatch3Func(t, true, "", "/foo/bar", "/foo/*") + testKeyMatch3Func(t, false, "", "/foobar", "/foo") + testKeyMatch3Func(t, false, "", "/foobar", "/foo*") + testKeyMatch3Func(t, false, "", "/foobar", "/foo/*") + + testKeyMatch3Func(t, false, "", "/", "/{resource}") + testKeyMatch3Func(t, true, "", "/resource1", "/{resource}") + testKeyMatch3Func(t, false, "", "/myid", "/{id}/using/{resId}") + testKeyMatch3Func(t, true, "", "/myid/using/myresid", "/{id}/using/{resId}") + + testKeyMatch3Func(t, false, "", "/proxy/myid", "/proxy/{id}/*") + testKeyMatch3Func(t, true, "", "/proxy/myid/", "/proxy/{id}/*") + testKeyMatch3Func(t, true, "", "/proxy/myid/res", "/proxy/{id}/*") + testKeyMatch3Func(t, true, "", "/proxy/myid/res/res2", "/proxy/{id}/*") + testKeyMatch3Func(t, true, "", "/proxy/myid/res/res2/res3", "/proxy/{id}/*") + testKeyMatch3Func(t, false, "", "/proxy/", "/proxy/{id}/*") +} + +func TestKeyGet3Func(t *testing.T) { + testKeyGet3Func(t, false, "keyGet3: expected 3 arguments, but got 2", "/myid/using/myresid", "/{id}/using/{resId}") + testKeyGet3Func(t, false, "keyGet3: expected 3 arguments, but got 4", "/myid/using/myresid", "/{id}/using/{resId}", "id", "resId") + testKeyGet3Func(t, false, "keyGet3: argument must be a string", "/myid/using/myresid", "/{id}/using/{resId}", true) + + testKeyGet3Func(t, "myid", "", "/myid/using/myresid", "/{id}/using/{resId}", "id") + testKeyGet3Func(t, "group_name", "", "/api/group1_group_name/project1_admin/info", "/api/{g}_{gn}/{proj}_admin/info", "gn") + testKeyGet3Func(t, "", "", "/myid/using/myresid", "/{id}/using/{resId}", "missing") +} + +func TestKeyMatch4Func(t *testing.T) { + testKeyMatch4Func(t, false, "keyMatch4: expected 2 arguments, but got 1", "/parent/123/child/123") + testKeyMatch4Func(t, false, "keyMatch4: expected 2 arguments, but got 3", "/parent/123/child/123", "/parent/{id}/child/{id}", true) + testKeyMatch4Func(t, false, "keyMatch4: argument must be a string", "/parent/123/child/123", true) + + testKeyMatch4Func(t, true, "", "/parent/123/child/123", "/parent/{id}/child/{id}") + testKeyMatch4Func(t, false, "", "/parent/123/child/456", "/parent/{id}/child/{id}") + + testKeyMatch4Func(t, true, "", "/parent/123/child/123", "/parent/{id}/child/{another_id}") + testKeyMatch4Func(t, true, "", "/parent/123/child/456", "/parent/{id}/child/{another_id}") +} + +func TestKeyMatch5Func(t *testing.T) { + testKeyMatch5Func(t, false, "keyMatch5: expected 2 arguments, but got 1", "/foo") + testKeyMatch5Func(t, false, "keyMatch5: expected 2 arguments, but got 3", "/foo/create/123", "/foo/*", "/foo/update/123") + testKeyMatch5Func(t, false, "keyMatch5: argument must be a string", "/parent/123", true) + + testKeyMatch5Func(t, true, "", "/parent/child?status=1&type=2", "/parent/child") + testKeyMatch5Func(t, false, "", "/parent?status=1&type=2", "/parent/child") + + testKeyMatch5Func(t, true, "", "/parent/child/?status=1&type=2", "/parent/child/") + testKeyMatch5Func(t, false, "", "/parent/child/?status=1&type=2", "/parent/child") + testKeyMatch5Func(t, false, "", "/parent/child?status=1&type=2", "/parent/child/") + + testKeyMatch5Func(t, true, "", "/foo", "/foo") + testKeyMatch5Func(t, true, "", "/foo", "/foo*") + testKeyMatch5Func(t, false, "", "/foo", "/foo/*") + testKeyMatch5Func(t, false, "", "/foo/bar", "/foo") + testKeyMatch5Func(t, false, "", "/foo/bar", "/foo*") + testKeyMatch5Func(t, true, "", "/foo/bar", "/foo/*") + testKeyMatch5Func(t, false, "", "/foobar", "/foo") + testKeyMatch5Func(t, false, "", "/foobar", "/foo*") + testKeyMatch5Func(t, false, "", "/foobar", "/foo/*") + + testKeyMatch5Func(t, false, "", "/", "/{resource}") + testKeyMatch5Func(t, true, "", "/resource1", "/{resource}") + testKeyMatch5Func(t, false, "", "/myid", "/{id}/using/{resId}") + testKeyMatch5Func(t, true, "", "/myid/using/myresid", "/{id}/using/{resId}") + + testKeyMatch5Func(t, false, "", "/proxy/myid", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/res", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/res/res2", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/res/res2/res3", "/proxy/{id}/*") + testKeyMatch5Func(t, false, "", "/proxy/", "/proxy/{id}/*") + + testKeyMatch5Func(t, false, "", "/proxy/myid?status=1&type=2", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/res?status=1&type=2", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/res/res2?status=1&type=2", "/proxy/{id}/*") + testKeyMatch5Func(t, true, "", "/proxy/myid/res/res2/res3?status=1&type=2", "/proxy/{id}/*") + testKeyMatch5Func(t, false, "", "/proxy/", "/proxy/{id}/*") +} + +func TestIPMatchFunc(t *testing.T) { + testIPMatchFunc(t, false, "ipMatch: expected 2 arguments, but got 1", "192.168.2.123") + testIPMatchFunc(t, false, "ipMatch: argument must be a string", "192.168.2.123", 128) + testIPMatchFunc(t, true, "", "192.168.2.123", "192.168.2.0/24") +} + +func TestGlobMatch(t *testing.T) { + testGlobMatch(t, "/foo", "/foo", true) + testGlobMatch(t, "/foo", "/foo*", true) + testGlobMatch(t, "/foo", "/foo/*", false) + testGlobMatch(t, "/foo/bar", "/foo", false) + testGlobMatch(t, "/foo/bar", "/foo*", false) + testGlobMatch(t, "/foo/bar", "/foo/*", true) + testGlobMatch(t, "/foobar", "/foo", false) + testGlobMatch(t, "/foobar", "/foo*", true) + testGlobMatch(t, "/foobar", "/foo/*", false) + + testGlobMatch(t, "/foo", "*/foo", true) + testGlobMatch(t, "/foo", "*/foo*", true) + testGlobMatch(t, "/foo", "*/foo/*", false) + testGlobMatch(t, "/foo/bar", "*/foo", false) + testGlobMatch(t, "/foo/bar", "*/foo*", false) + testGlobMatch(t, "/foo/bar", "*/foo/*", true) + testGlobMatch(t, "/foobar", "*/foo", false) + testGlobMatch(t, "/foobar", "*/foo*", true) + testGlobMatch(t, "/foobar", "*/foo/*", false) + + testGlobMatch(t, "/prefix/foo", "*/foo", false) + testGlobMatch(t, "/prefix/foo", "*/foo*", false) + testGlobMatch(t, "/prefix/foo", "*/foo/*", false) + testGlobMatch(t, "/prefix/foo/bar", "*/foo", false) + testGlobMatch(t, "/prefix/foo/bar", "*/foo*", false) + testGlobMatch(t, "/prefix/foo/bar", "*/foo/*", false) + testGlobMatch(t, "/prefix/foobar", "*/foo", false) + testGlobMatch(t, "/prefix/foobar", "*/foo*", false) + testGlobMatch(t, "/prefix/foobar", "*/foo/*", false) + + testGlobMatch(t, "/prefix/subprefix/foo", "*/foo", false) + testGlobMatch(t, "/prefix/subprefix/foo", "*/foo*", false) + testGlobMatch(t, "/prefix/subprefix/foo", "*/foo/*", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "*/foo", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "*/foo*", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "*/foo/*", false) + testGlobMatch(t, "/prefix/subprefix/foobar", "*/foo", false) + testGlobMatch(t, "/prefix/subprefix/foobar", "*/foo*", false) + testGlobMatch(t, "/prefix/subprefix/foobar", "*/foo/*", false) + + testGlobMatch(t, "/foo", "**/foo", true) + testGlobMatch(t, "/foo", "**/foo**", true) + testGlobMatch(t, "/foo", "**/foo/**", true) + testGlobMatch(t, "/foo/bar", "**/foo", false) + testGlobMatch(t, "/foo/bar", "**/foo**", false) + testGlobMatch(t, "/foo/bar", "**/foo/**", true) + testGlobMatch(t, "/foobar", "**/foo", false) + testGlobMatch(t, "/foobar", "**/foo**", true) + testGlobMatch(t, "/foobar", "**/foo/**", false) + + testGlobMatch(t, "/prefix/foo", "**/foo", true) + testGlobMatch(t, "/prefix/foo", "**/foo**", true) + testGlobMatch(t, "/prefix/foo", "**/foo/**", true) + testGlobMatch(t, "/prefix/foo/bar", "**/foo", false) + testGlobMatch(t, "/prefix/foo/bar", "**/foo**", false) + testGlobMatch(t, "/prefix/foo/bar", "**/foo/**", true) + testGlobMatch(t, "/prefix/foobar", "**/foo", false) + testGlobMatch(t, "/prefix/foobar", "**/foo**", true) + testGlobMatch(t, "/prefix/foobar", "**/foo/**", false) + + testGlobMatch(t, "/prefix/subprefix/foo", "**/foo", true) + testGlobMatch(t, "/prefix/subprefix/foo", "**/foo**", true) + testGlobMatch(t, "/prefix/subprefix/foo", "**/foo/**", true) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "**/foo", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "**/foo**", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "**/foo/**", true) + testGlobMatch(t, "/prefix/subprefix/foobar", "**/foo", false) + testGlobMatch(t, "/prefix/subprefix/foobar", "**/foo**", true) + testGlobMatch(t, "/prefix/subprefix/foobar", "**/foo/**", false) + + testGlobMatch(t, "/foo", "*/foo**", true) + testGlobMatch(t, "/foo", "**/foo*", true) + testGlobMatch(t, "/foo", "*/foo/**", true) + testGlobMatch(t, "/foo", "**/foo/*", false) + testGlobMatch(t, "/foo/bar", "*/foo**", false) + testGlobMatch(t, "/foo/bar", "**/foo*", false) + testGlobMatch(t, "/foo/bar", "*/foo/**", true) + testGlobMatch(t, "/foo/bar", "**/foo/*", true) + testGlobMatch(t, "/foobar", "*/foo**", true) + testGlobMatch(t, "/foobar", "**/foo*", true) + testGlobMatch(t, "/foobar", "*/foo/**", false) + testGlobMatch(t, "/foobar", "**/foo/*", false) + + testGlobMatch(t, "/prefix/foo", "*/foo**", false) + testGlobMatch(t, "/prefix/foo", "**/foo*", true) + testGlobMatch(t, "/prefix/foo", "*/foo/**", false) + testGlobMatch(t, "/prefix/foo", "**/foo/*", false) + testGlobMatch(t, "/prefix/foo/bar", "*/foo**", false) + testGlobMatch(t, "/prefix/foo/bar", "**/foo*", false) + testGlobMatch(t, "/prefix/foo/bar", "*/foo/**", false) + testGlobMatch(t, "/prefix/foo/bar", "**/foo/*", true) + testGlobMatch(t, "/prefix/foobar", "*/foo**", false) + testGlobMatch(t, "/prefix/foobar", "**/foo*", true) + testGlobMatch(t, "/prefix/foobar", "*/foo/**", false) + testGlobMatch(t, "/prefix/foobar", "**/foo/*", false) + + testGlobMatch(t, "/prefix/subprefix/foo", "*/foo**", false) + testGlobMatch(t, "/prefix/subprefix/foo", "**/foo*", true) + testGlobMatch(t, "/prefix/subprefix/foo", "*/foo/**", false) + testGlobMatch(t, "/prefix/subprefix/foo", "**/foo/*", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "*/foo**", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "**/foo*", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "*/foo/**", false) + testGlobMatch(t, "/prefix/subprefix/foo/bar", "**/foo/*", true) + testGlobMatch(t, "/prefix/subprefix/foobar", "*/foo**", false) + testGlobMatch(t, "/prefix/subprefix/foobar", "**/foo*", true) + testGlobMatch(t, "/prefix/subprefix/foobar", "*/foo/**", false) + testGlobMatch(t, "/prefix/subprefix/foobar", "**/foo/*", false) +} + +func testTimeMatch(t *testing.T, startTime string, endTime string, res bool) { + t.Helper() + myRes, err := TimeMatch(startTime, endTime) + if err != nil { + panic(err) + } + t.Logf("%s < %s: %t", startTime, endTime, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", startTime, endTime, !res, res) + } +} + +func TestTestMatch(t *testing.T) { + testTimeMatch(t, "0000-01-01 00:00:00", "0000-01-02 00:00:00", false) + testTimeMatch(t, "0000-01-01 00:00:00", "9999-12-30 00:00:00", true) + testTimeMatch(t, "_", "_", true) + testTimeMatch(t, "_", "9999-12-30 00:00:00", true) + testTimeMatch(t, "_", "0000-01-02 00:00:00", false) + testTimeMatch(t, "0000-01-01 00:00:00", "_", true) + testTimeMatch(t, "9999-12-30 00:00:00", "_", false) +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..4311dcd --- /dev/null +++ b/util/util.go @@ -0,0 +1,441 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package util + +import ( + "encoding/json" + "regexp" + "sort" + "strings" + "sync" +) + +var evalReg = regexp.MustCompile(`\beval\((?P[^)]*)\)`) + +var escapeAssertionRegex = regexp.MustCompile(`([()\s|&,=!><+\-*/]|^)((r|p)[0-9]*)\.`) + +func JsonToMap(jsonStr string) (map[string]interface{}, error) { + result := make(map[string]interface{}) + err := json.Unmarshal([]byte(jsonStr), &result) + if err != nil { + return result, err + } + return result, nil +} + +// EscapeAssertion escapes the dots in the assertion, because the expression evaluation doesn't support such variable names. +func EscapeAssertion(s string) string { + s = escapeAssertionRegex.ReplaceAllStringFunc(s, func(m string) string { + // Replace only the last dot with underscore (preserve the prefix character) + lastDotIdx := strings.LastIndex(m, ".") + if lastDotIdx > 0 { + return m[:lastDotIdx] + "_" + } + return m + }) + return s +} + +// RemoveComments removes the comments starting with # in the text. +func RemoveComments(s string) string { + pos := strings.Index(s, "#") + if pos == -1 { + return s + } + return strings.TrimSpace(s[0:pos]) +} + +// ArrayEquals determines whether two string arrays are identical. +func ArrayEquals(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +// Array2DEquals determines whether two 2-dimensional string arrays are identical. +func Array2DEquals(a [][]string, b [][]string) bool { + if len(a) != len(b) { + return false + } + + for i, v := range a { + if !ArrayEquals(v, b[i]) { + return false + } + } + return true +} + +// SortArray2D Sorts the two-dimensional string array. +func SortArray2D(arr [][]string) { + if len(arr) == 0 { + return + } + sort.Slice(arr, func(i, j int) bool { + minArrLen := len(arr[i]) + if len(arr[j]) < minArrLen { + minArrLen = len(arr[j]) + } + for k := 0; k < minArrLen; k++ { + if arr[i][k] != arr[j][k] { + return arr[i][k] < arr[j][k] + } + } + return len(arr[i]) < len(arr[j]) + }) +} + +// SortedArray2DEquals determines whether two 2-dimensional string arrays are identical. +func SortedArray2DEquals(a [][]string, b [][]string) bool { + if len(a) != len(b) { + return false + } + copyA := make([][]string, len(a)) + copy(copyA, a) + copyB := make([][]string, len(b)) + copy(copyB, b) + + SortArray2D(copyA) + SortArray2D(copyB) + + for i, v := range copyA { + if !ArrayEquals(v, copyB[i]) { + return false + } + } + return true +} + +// ArrayRemoveDuplicates removes any duplicated elements in a string array. +func ArrayRemoveDuplicates(s *[]string) { + found := make(map[string]bool) + j := 0 + for i, x := range *s { + if !found[x] { + found[x] = true + (*s)[j] = (*s)[i] + j++ + } + } + *s = (*s)[:j] +} + +// ArrayToString gets a printable string for a string array. +func ArrayToString(s []string) string { + return strings.Join(s, ", ") +} + +// ParamsToString gets a printable string for variable number of parameters. +func ParamsToString(s ...string) string { + return strings.Join(s, ", ") +} + +// SetEquals determines whether two string sets are identical. +func SetEquals(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + + count := make(map[string]int, len(a)) + for _, v := range a { + count[v]++ + } + for _, v := range b { + count[v]-- + if count[v] < 0 { + return false + } + } + + return true +} + +// SetEqualsInt determines whether two int sets are identical. +func SetEqualsInt(a []int, b []int) bool { + if len(a) != len(b) { + return false + } + + count := make(map[int]int, len(a)) + for _, v := range a { + count[v]++ + } + for _, v := range b { + count[v]-- + if count[v] < 0 { + return false + } + } + + return true +} + +// Set2DEquals determines whether two string slice sets are identical. +func Set2DEquals(a [][]string, b [][]string) bool { + if len(a) != len(b) { + return false + } + + return SetEquals(setKeys(a), setKeys(b)) +} + +// setKeys maps every row to an order-independent key, without touching the row. +// The fields are joined on a NUL because a policy value may contain a comma. +func setKeys(rows [][]string) []string { + keys := make([]string, len(rows)) + + for i, row := range rows { + fields := make([]string, len(row)) + copy(fields, row) + sort.Strings(fields) + keys[i] = strings.Join(fields, "\x00") + } + + return keys +} + +// JoinSlice joins a string and a slice into a new slice. +func JoinSlice(a string, b ...string) []string { + res := make([]string, 0, len(b)+1) + + res = append(res, a) + res = append(res, b...) + + return res +} + +// JoinSliceAny joins a string and a slice into a new interface{} slice. +func JoinSliceAny(a string, b ...string) []interface{} { + res := make([]interface{}, 0, len(b)+1) + + res = append(res, a) + for _, s := range b { + res = append(res, s) + } + + return res +} + +// SetSubtract returns the elements in `a` that aren't in `b`. +func SetSubtract(a []string, b []string) []string { + mb := make(map[string]struct{}, len(b)) + for _, x := range b { + mb[x] = struct{}{} + } + var diff []string + for _, x := range a { + if _, found := mb[x]; !found { + diff = append(diff, x) + } + } + return diff +} + +// HasEval determine whether matcher contains function eval. +func HasEval(s string) bool { + return evalReg.MatchString(s) +} + +// ReplaceEval replace function eval with the value of its parameters. +func ReplaceEval(s string, rule string) string { + return evalReg.ReplaceAllString(s, "("+rule+")") +} + +// ReplaceEvalWithMap replace function eval with the value of its parameters via given sets. +func ReplaceEvalWithMap(src string, sets map[string]string) string { + return evalReg.ReplaceAllStringFunc(src, func(s string) string { + subs := evalReg.FindStringSubmatch(s) + if subs == nil { + return s + } + key := subs[1] + value, found := sets[key] + if !found { + return s + } + return evalReg.ReplaceAllString(s, value) + }) +} + +// GetEvalValue returns the parameters of function eval. +func GetEvalValue(s string) []string { + subMatch := evalReg.FindAllStringSubmatch(s, -1) + var rules []string + for _, rule := range subMatch { + rules = append(rules, rule[1]) + } + return rules +} + +// EscapeStringLiterals escapes backslashes in string literals within an expression +// to ensure consistent handling between govaluate (which interprets escape sequences) +// and CSV parsing (which treats backslashes as literal characters). +// This function doubles all backslashes within single-quoted and double-quoted strings. +func EscapeStringLiterals(expr string) string { + var result strings.Builder + inString := false + var quote rune + + for _, ch := range expr { + if inString { + result.WriteRune(ch) + switch ch { + case '\\': + // Found a backslash inside a string - double it + result.WriteRune('\\') + case quote: + // End of string literal + inString = false + } + continue + } + + // Not inside a string literal + if ch == '\'' || ch == '"' { + inString = true + quote = ch + } + result.WriteRune(ch) + } + + return result.String() +} + +func RemoveDuplicateElement(s []string) []string { + result := make([]string, 0, len(s)) + temp := map[string]struct{}{} + for _, item := range s { + if _, ok := temp[item]; !ok { + temp[item] = struct{}{} + result = append(result, item) + } + } + return result +} + +type node struct { + key interface{} + value interface{} + prev *node + next *node +} + +type LRUCache struct { + capacity int + m map[interface{}]*node + head *node + tail *node +} + +func NewLRUCache(capacity int) *LRUCache { + cache := &LRUCache{} + cache.capacity = capacity + cache.m = map[interface{}]*node{} + + head := &node{} + tail := &node{} + + head.next = tail + tail.prev = head + + cache.head = head + cache.tail = tail + + return cache +} + +func (cache *LRUCache) remove(n *node, listOnly bool) { + if !listOnly { + delete(cache.m, n.key) + } + n.prev.next = n.next + n.next.prev = n.prev +} + +func (cache *LRUCache) add(n *node, listOnly bool) { + if !listOnly { + cache.m[n.key] = n + } + headNext := cache.head.next + cache.head.next = n + headNext.prev = n + n.next = headNext + n.prev = cache.head +} + +func (cache *LRUCache) moveToHead(n *node) { + cache.remove(n, true) + cache.add(n, true) +} + +func (cache *LRUCache) Get(key interface{}) (value interface{}, ok bool) { + n, ok := cache.m[key] + if ok { + cache.moveToHead(n) + return n.value, ok + } else { + return nil, ok + } +} + +func (cache *LRUCache) Put(key interface{}, value interface{}) { + if cache.capacity <= 0 { + return + } + + n, ok := cache.m[key] + if ok { + cache.remove(n, false) + } else { + n = &node{key, value, nil, nil} + if len(cache.m) >= cache.capacity { + cache.remove(cache.tail.prev, false) + } + } + cache.add(n, false) +} + +type SyncLRUCache struct { + rwm sync.RWMutex + *LRUCache +} + +func NewSyncLRUCache(capacity int) *SyncLRUCache { + cache := &SyncLRUCache{} + cache.LRUCache = NewLRUCache(capacity) + return cache +} + +func (cache *SyncLRUCache) Get(key interface{}) (value interface{}, ok bool) { + cache.rwm.Lock() + defer cache.rwm.Unlock() + return cache.LRUCache.Get(key) +} + +func (cache *SyncLRUCache) Put(key interface{}, value interface{}) { + cache.rwm.Lock() + defer cache.rwm.Unlock() + cache.LRUCache.Put(key, value) +} diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 0000000..45082b5 --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,281 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package util + +import ( + "testing" +) + +func testEscapeAssertion(t *testing.T, s string, res string) { + t.Helper() + myRes := EscapeAssertion(s) + t.Logf("%s: %s", s, myRes) + + if myRes != res { + t.Errorf("%s: %s, supposed to be %s", s, myRes, res) + } +} + +func TestEscapeAssertion(t *testing.T) { + testEscapeAssertion(t, "r_sub == r_obj.value", "r_sub == r_obj.value") + testEscapeAssertion(t, "p_sub == r_sub.value", "p_sub == r_sub.value") + testEscapeAssertion(t, "r.attr.value == p.attr", "r_attr.value == p_attr") + testEscapeAssertion(t, "r.attr.value == p.attr", "r_attr.value == p_attr") + testEscapeAssertion(t, "r.attp.value || p.attr", "r_attp.value || p_attr") + testEscapeAssertion(t, "r2.attr.value == p2.attr", "r2_attr.value == p2_attr") + testEscapeAssertion(t, "r2.attp.value || p2.attr", "r2_attp.value || p2_attr") + testEscapeAssertion(t, "r.attp.value &&p.attr", "r_attp.value &&p_attr") + testEscapeAssertion(t, "r.attp.value >p.attr", "r_attp.value >p_attr") + testEscapeAssertion(t, "r.attp.value 0 { + if s, isString := rvals[0].(string); isString { + entry.Subject = s + } + } + if len(rvals) > 1 { + if o, isString := rvals[1].(string); isString { + entry.Object = o + } + } + if len(rvals) > 2 { + if a, isString := rvals[2].(string); isString { + entry.Action = a + } + } + if len(rvals) > 3 { + if d, isString := rvals[3].(string); isString { + entry.Domain = d + } + } + return entry +} + +// onLogBeforeEventInEnforce initializes logging for Enforce operation. +func (e *Enforcer) onLogBeforeEventInEnforce(rvals []interface{}) *log.LogEntry { + if e.logger == nil { + return nil + } + logEntry := e.createEnforceLogEntry(rvals) + _ = e.logger.OnBeforeEvent(logEntry) + return logEntry +} + +// onLogAfterEventInEnforce finalizes logging for Enforce operation. +func (e *Enforcer) onLogAfterEventInEnforce(logEntry *log.LogEntry, allowed bool) { + if e.logger != nil && logEntry != nil { + logEntry.Allowed = allowed + _ = e.logger.OnAfterEvent(logEntry) + } +} + +// logPolicyOperation logs a policy operation (add or remove) with before and after events. +func (e *Enforcer) logPolicyOperation(eventType log.EventType, sec string, rule []string, operation func() (bool, error)) (bool, error) { + var logEntry *log.LogEntry + if e.logger != nil && sec == "p" { + logEntry = &log.LogEntry{ + EventType: eventType, + Rules: [][]string{rule}, + } + _ = e.logger.OnBeforeEvent(logEntry) + } + + ok, err := operation() + + if e.logger != nil && logEntry != nil { + if ok && err == nil { + logEntry.RuleCount = 1 + } else { + logEntry.RuleCount = 0 + logEntry.Error = err + } + _ = e.logger.OnAfterEvent(logEntry) + } + + return ok, err +} diff --git a/watcher_ex_test.go b/watcher_ex_test.go new file mode 100644 index 0000000..c345281 --- /dev/null +++ b/watcher_ex_test.go @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" + + "github.com/casbin/casbin/v3/model" +) + +type SampleWatcherEx struct { + SampleWatcher +} + +func (w SampleWatcherEx) UpdateForAddPolicy(sec, ptype string, params ...string) error { + return nil +} + +func (w SampleWatcherEx) UpdateForRemovePolicy(sec, ptype string, params ...string) error { + return nil +} + +func (w SampleWatcherEx) UpdateForRemoveFilteredPolicy(sec, ptype string, fieldIndex int, fieldValues ...string) error { + return nil +} + +func (w SampleWatcherEx) UpdateForSavePolicy(model model.Model) error { + return nil +} + +func (w SampleWatcherEx) UpdateForAddPolicies(sec string, ptype string, rules ...[]string) error { + return nil +} + +func (w SampleWatcherEx) UpdateForRemovePolicies(sec string, ptype string, rules ...[]string) error { + return nil +} + +func TestSetWatcherEx(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + sampleWatcherEx := &SampleWatcherEx{} + err := e.SetWatcher(sampleWatcherEx) + if err != nil { + t.Fatal(err) + } + + _ = e.SavePolicy() // calls watcherEx.UpdateForSavePolicy() + _, _ = e.AddPolicy("admin", "data1", "read") // calls watcherEx.UpdateForAddPolicy() + _, _ = e.RemovePolicy("admin", "data1", "read") // calls watcherEx.UpdateForRemovePolicy() + _, _ = e.RemoveFilteredPolicy(1, "data1") // calls watcherEx.UpdateForRemoveFilteredPolicy() + _, _ = e.RemovePolicy("admin", "data1", "read") // calls watcherEx.UpdateForRemovePolicy() + _, _ = e.AddGroupingPolicy("g:admin", "data1") + _, _ = e.RemoveGroupingPolicy("g:admin", "data1") + _, _ = e.AddGroupingPolicy("g:admin", "data1") + _, _ = e.RemoveFilteredGroupingPolicy(1, "data1") + _, _ = e.AddPolicies([][]string{{"admin", "data1", "read"}, {"admin", "data2", "read"}}) // calls watcherEx.UpdateForAddPolicies() + _, _ = e.RemovePolicies([][]string{{"admin", "data1", "read"}, {"admin", "data2", "read"}}) // calls watcherEx.UpdateForRemovePolicies() +} diff --git a/watcher_test.go b/watcher_test.go new file mode 100644 index 0000000..3db3cea --- /dev/null +++ b/watcher_test.go @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import "testing" + +type SampleWatcher struct { + callback func(string) +} + +func (w *SampleWatcher) Close() { +} + +func (w *SampleWatcher) SetUpdateCallback(callback func(string)) error { + w.callback = callback + return nil +} + +func (w *SampleWatcher) Update() error { + if w.callback != nil { + w.callback("") + } + return nil +} + +func TestSetWatcher(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatal(err) + } + sampleWatcher := &SampleWatcher{} + err = e.SetWatcher(sampleWatcher) + if err != nil { + t.Fatal(err) + } + err = e.SavePolicy() // calls watcher.Update() + if err != nil { + t.Fatal(err) + } +} + +func TestSelfModify(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatal(err) + } + + sampleWatcher := &SampleWatcher{} + err = e.SetWatcher(sampleWatcher) + if err != nil { + t.Fatal(err) + } + + var called int + + called = -1 + _ = e.watcher.SetUpdateCallback(func(s string) { + called = 1 + }) + _, err = e.AddPolicy("eva", "data", "read") // calls watcher.Update() + if err != nil { + t.Fatal(err) + } + if called != 1 { + t.Fatal("callback should be called") + } + + called = -1 + _ = e.watcher.SetUpdateCallback(func(s string) { + called = 1 + }) + _, err = e.SelfAddPolicy("p", "p", []string{"eva", "data", "write"}) // calls watcher.Update() + if err != nil { + t.Fatal(err) + } + if called != -1 { + t.Fatal("callback should not be called") + } +} diff --git a/watcher_update_test.go b/watcher_update_test.go new file mode 100644 index 0000000..04d7fe4 --- /dev/null +++ b/watcher_update_test.go @@ -0,0 +1,43 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package casbin + +import ( + "testing" +) + +type SampleWatcherUpdatable struct { + SampleWatcher +} + +func (w SampleWatcherUpdatable) UpdateForUpdatePolicy(params ...string) error { + return nil +} + +func TestSetWatcherUpdatable(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + sampleWatcherEx := &SampleWatcherUpdatable{} + err := e.SetWatcher(sampleWatcherEx) + if err != nil { + t.Fatal(err) + } + + _ = e.SavePolicy() // calls watcherEx.UpdateForSavePolicy() + _, _ = e.UpdatePolicy([]string{"admin", "data1", "read"}, []string{"admin", "data2", "read"}) // calls watcherEx.UpdateForUpdatePolicy() +}