diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 000000000..00cedfcb4
--- /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/.github/scripts/benchmark_formatter.py b/.github/scripts/benchmark_formatter.py
new file mode 100644
index 000000000..89247d18f
--- /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 000000000..a3fddde87
--- /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 000000000..e7c0a1062
--- /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 000000000..605f6a7f9
--- /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 000000000..feb02e33c
--- /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 000000000..86bdc0fb2
--- /dev/null
+++ b/.github/workflows/default.yml
@@ -0,0 +1,59 @@
+name: Build
+
+on:
+ push:
+ branches:
+ - master
+ 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
+
+ semantic-release:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Setup Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20
+
+ - name: Run semantic-release
+ if: github.repository == 'casbin/casbin' && github.event_name == 'push'
+ run: make release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
new file mode 100644
index 000000000..77912565b
--- /dev/null
+++ b/.github/workflows/golangci-lint.yml
@@ -0,0 +1,24 @@
+name: golangci-lint
+
+on:
+ push:
+ branches:
+ - master
+ - main
+ 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@v4
+ with:
+ version: v1.56.2
\ 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 000000000..cda633c5f
--- /dev/null
+++ b/.github/workflows/performance-pr.yml
@@ -0,0 +1,93 @@
+name: Performance Comparison for Pull Requests
+
+on:
+ pull_request:
+ branches: [master]
+
+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/.gitignore b/.gitignore
index 72ed6f474..da27805f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,7 @@ _testmain.go
*.prof
.idea/
-*.iml
\ No newline at end of file
+*.iml
+
+# vendor files
+vendor
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 000000000..b8d362019
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,354 @@
+# 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
+
+## Golden config for golangci-lint v1.56.2
+#
+# This is the best config for golangci-lint based on my experience and opinion.
+# It is very strict, but not extremely strict.
+# Feel free to adapt and change it for your needs.
+
+run:
+ # Timeout for analysis, e.g. 30s, 5m.
+ # Default: 1m
+ timeout: 3m
+
+
+# This file contains only configs which differ from defaults.
+# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
+linters-settings:
+ cyclop:
+ # The maximal code complexity to report.
+ # Default: 10
+ max-complexity: 30
+ # The maximal average package complexity.
+ # If it's higher than 0.0 (float) the check is enabled
+ # Default: 0.0
+ package-average: 10.0
+
+ errcheck:
+ # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
+ # Such cases aren't reported by default.
+ # Default: false
+ check-type-assertions: true
+
+ exhaustive:
+ # Program elements to check for exhaustiveness.
+ # Default: [ switch ]
+ check:
+ - switch
+ - map
+
+ exhaustruct:
+ # List of regular expressions to exclude struct packages and their names from checks.
+ # Regular expressions must match complete canonical struct package/name/structname.
+ # Default: []
+ exclude:
+ # std libs
+ - "^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$"
+ # public libs
+ - "^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:
+ # Checks the number of lines in a function.
+ # If lower than 0, disable the check.
+ # Default: 60
+ lines: 100
+ # Checks the number of statements in a function.
+ # If lower than 0, disable the check.
+ # Default: 40
+ statements: 50
+ # Ignore comments when counting lines.
+ # Default false
+ ignore-comments: true
+
+ gocognit:
+ # Minimal code complexity to report.
+ # Default: 30 (but we recommend 10-20)
+ min-complexity: 20
+
+ gocritic:
+ # Settings passed to gocritic.
+ # The settings key is the name of a supported gocritic checker.
+ # The list of supported checkers can be find in https://go-critic.github.io/overview.
+ settings:
+ captLocal:
+ # Whether to restrict checker to params only.
+ # Default: true
+ paramsOnly: false
+ underef:
+ # Whether to skip (*x).method() calls where x is a pointer receiver.
+ # Default: true
+ skipRecvDeref: false
+
+ gomnd:
+ # List of function patterns to exclude from analysis.
+ # Values always ignored: `time.Date`,
+ # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
+ # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
+ # Default: []
+ ignored-functions:
+ - flag.Arg
+ - flag.Duration.*
+ - flag.Float.*
+ - flag.Int.*
+ - flag.Uint.*
+ - os.Chmod
+ - os.Mkdir.*
+ - os.OpenFile
+ - os.WriteFile
+ - prometheus.ExponentialBuckets.*
+ - prometheus.LinearBuckets
+
+ gomodguard:
+ blocked:
+ # List of blocked modules.
+ # Default: []
+ modules:
+ - github.com/golang/protobuf:
+ recommendations:
+ - google.golang.org/protobuf
+ reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
+ - github.com/satori/go.uuid:
+ recommendations:
+ - github.com/google/uuid
+ reason: "satori's package is not maintained"
+ - github.com/gofrs/uuid:
+ recommendations:
+ - github.com/gofrs/uuid/v5
+ reason: "gofrs' package was not go module before v5"
+
+ govet:
+ # Enable all analyzers.
+ # Default: false
+ enable-all: true
+ # Disable analyzers by name.
+ # Run `go tool vet help` to see all analyzers.
+ # Default: []
+ disable:
+ - fieldalignment # too strict
+ # Settings per analyzer.
+ settings:
+ shadow:
+ # Whether to be strict about shadowing; can be noisy.
+ # Default: false
+ #strict: true
+
+ inamedparam:
+ # Skips check for interface methods with only a single parameter.
+ # Default: false
+ skip-single-param: true
+
+ nakedret:
+ # Make an issue if func has more lines of code than this setting, and it has naked returns.
+ # Default: 30
+ max-func-lines: 0
+
+ nolintlint:
+ # Exclude following linters from requiring an explanation.
+ # Default: []
+ allow-no-explanation: [ funlen, gocognit, lll ]
+ # Enable to require an explanation of nonzero length after each nolint directive.
+ # Default: false
+ require-explanation: true
+ # Enable to require nolint directives to mention the specific linter being suppressed.
+ # Default: false
+ require-specific: true
+
+ perfsprint:
+ # Optimizes into strings concatenation.
+ # Default: true
+ strconcat: false
+
+ rowserrcheck:
+ # database/sql is always checked
+ # Default: []
+ packages:
+ - github.com/jmoiron/sqlx
+
+ tenv:
+ # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
+ # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
+ # Default: false
+ all: true
+
+ stylecheck:
+ # STxxxx checks in https://staticcheck.io/docs/configuration/options/#checks
+ # Default: ["*"]
+ checks: ["all", "-ST1003"]
+
+ revive:
+ rules:
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter
+ - name: unused-parameter
+ disabled: true
+
+linters:
+ disable-all: true
+ enable:
+ ## enabled by default
+ #- errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
+ - gosimple # specializes in simplifying a code
+ - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
+ - ineffassign # detects when assignments to existing variables are not used
+ - staticcheck # is a go vet on steroids, applying a ton of static analysis checks
+ - typecheck # like the front-end of a Go compiler, parses and type-checks Go code
+ - unused # checks for unused constants, variables, functions and types
+ ## disabled by default
+ - asasalint # checks for pass []any as any in variadic func(...any)
+ - asciicheck # checks that your code does not contain non-ASCII identifiers
+ - bidichk # checks for dangerous unicode character sequences
+ - bodyclose # checks whether HTTP response body is closed successfully
+ - cyclop # checks function and package cyclomatic complexity
+ - dupl # tool for code clone detection
+ - durationcheck # checks for two durations multiplied together
+ - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
+ #- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
+ - execinquery # checks query string in Query function which reads your Go src files and warning it finds
+ - exhaustive # checks exhaustiveness of enum switch statements
+ - exportloopref # checks for pointers to enclosing loop variables
+ #- forbidigo # forbids identifiers
+ - funlen # tool for detection of long functions
+ - gocheckcompilerdirectives # validates go compiler directive comments (//go:)
+ #- gochecknoglobals # checks that no global variables exist
+ - gochecknoinits # checks that no init functions are present in Go code
+ - gochecksumtype # checks exhaustiveness on Go "sum types"
+ #- gocognit # computes and checks the cognitive complexity of functions
+ #- goconst # finds repeated strings that could be replaced by a constant
+ #- gocritic # provides diagnostics that check for bugs, performance and style issues
+ - gocyclo # computes and checks the cyclomatic complexity of functions
+ - godot # checks if comments end in a period
+ - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
+ #- gomnd # detects magic numbers
+ - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
+ - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
+ - goprintffuncname # checks that printf-like functions are named with f at the end
+ - gosec # inspects source code for security problems
+ #- lll # reports long lines
+ - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
+ - makezero # finds slice declarations with non-zero initial length
+ - mirror # reports wrong mirror patterns of bytes/strings usage
+ - musttag # enforces field tags in (un)marshaled structs
+ - nakedret # finds naked returns in functions greater than a specified function length
+ - nestif # reports deeply nested if statements
+ - nilerr # finds the code that returns nil even if it checks that the error is not nil
+ #- nilnil # checks that there is no simultaneous return of nil error and an invalid value
+ - noctx # finds sending http request without context.Context
+ - nolintlint # reports ill-formed or insufficient nolint directives
+ #- nonamedreturns # reports all named returns
+ - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
+ #- perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
+ - predeclared # finds code that shadows one of Go's predeclared identifiers
+ - promlinter # checks Prometheus metrics naming via promlint
+ - protogetter # reports direct reads from proto message fields when getters should be used
+ - reassign # checks that package variables are not reassigned
+ - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
+ - rowserrcheck # checks whether Err of rows is checked successfully
+ - sloglint # ensure consistent code style when using log/slog
+ - spancheck # checks for mistakes with OpenTelemetry/Census spans
+ - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
+ - stylecheck # is a replacement for golint
+ - tenv # detects using os.Setenv instead of t.Setenv since Go1.17
+ - testableexamples # checks if examples are testable (have an expected output)
+ - testifylint # checks usage of github.com/stretchr/testify
+ #- testpackage # makes you use a separate _test package
+ - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
+ - unconvert # removes unnecessary type conversions
+ #- unparam # reports unused function parameters
+ - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
+ - wastedassign # finds wasted assignment statements
+ - whitespace # detects leading and trailing whitespace
+
+ ## you may want to enable
+ #- decorder # checks declaration order and count of types, constants, variables and functions
+ #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized
+ #- gci # controls golang package import order and makes it always deterministic
+ #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega
+ #- godox # detects FIXME, TODO and other comment keywords
+ #- goheader # checks is file header matches to pattern
+ #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters
+ #- interfacebloat # checks the number of methods inside an interface
+ #- ireturn # accept interfaces, return concrete types
+ #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
+ #- tagalign # checks that struct tags are well aligned
+ #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope
+ #- wrapcheck # checks that errors returned from external packages are wrapped
+ #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event
+
+ ## disabled
+ #- containedctx # detects struct contained context.Context field
+ #- contextcheck # [too many false positives] checks the function whether use a non-inherited context
+ #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages
+ #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
+ #- dupword # [useless without config] checks for duplicate words in the source code
+ #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted
+ #- forcetypeassert # [replaced by errcheck] finds forced type assertions
+ #- goerr113 # [too strict] checks the errors handling expressions
+ #- gofmt # [replaced by goimports] checks whether code was gofmt-ed
+ #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed
+ #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase
+ #- grouper # analyzes expression groups
+ #- importas # enforces consistent import aliases
+ #- maintidx # measures the maintainability index of each function
+ #- misspell # [useless] finds commonly misspelled English words in comments
+ #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
+ #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test
+ #- tagliatelle # checks the struct tags
+ #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers
+ #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
+
+ ## deprecated
+ #- deadcode # [deprecated, replaced by unused] finds unused code
+ #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized
+ #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
+ #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible
+ #- interfacer # [deprecated] suggests narrower interface types
+ #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted
+ #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name
+ #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs
+ #- structcheck # [deprecated, replaced by unused] finds unused struct fields
+ #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants
+
+
+issues:
+ # Maximum count of issues with the same text.
+ # Set to 0 to disable.
+ # Default: 3
+ max-same-issues: 50
+
+ exclude-rules:
+ - source: "(noinspection|TODO)"
+ linters: [ godot ]
+ - source: "//noinspection"
+ linters: [ gocritic ]
+ - path: "_test\\.go"
+ linters:
+ - bodyclose
+ - dupl
+ - funlen
+ - goconst
+ - gosec
+ - noctx
+ - wrapcheck
+ # TODO: remove after PR is released https://github.com/golangci/golangci-lint/pull/4386
+ - text: "fmt.Sprintf can be replaced with string addition"
+ linters: [ perfsprint ]
\ No newline at end of file
diff --git a/.releaserc.json b/.releaserc.json
new file mode 100644
index 000000000..58cb0bb4c
--- /dev/null
+++ b/.releaserc.json
@@ -0,0 +1,16 @@
+{
+ "debug": true,
+ "branches": [
+ "+([0-9])?(.{+([0-9]),x}).x",
+ "master",
+ {
+ "name": "beta",
+ "prerelease": true
+ }
+ ],
+ "plugins": [
+ "@semantic-release/commit-analyzer",
+ "@semantic-release/release-notes-generator",
+ "@semantic-release/github"
+ ]
+}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index be7f63415..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-language: go
-
-sudo: false
-
-go:
- - tip
-
-before_install:
- - go get github.com/mattn/goveralls
-
-script:
- - $HOME/gopath/bin/goveralls -service=travis-ci
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..4bab59c93
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,35 @@
+# How to contribute
+
+The following is a set of guidelines for contributing to casbin and its libraries, which are hosted at [casbin organization at Github](https://github.com/casbin).
+
+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.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/casbin/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 000000000..4b488f591
--- /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/Makefile b/Makefile
new file mode 100644
index 000000000..64546af2a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+SHELL = /bin/bash
+export PATH := $(shell yarn global bin):$(PATH)
+
+default: lint test
+
+test:
+ go test -race -v ./...
+
+benchmark:
+ go test -bench=.
+
+lint:
+ golangci-lint run --verbose
+
+release:
+ npx semantic-release@v19.0.2
diff --git a/README.md b/README.md
index 1392d40ef..89dc9af8e 100644
--- a/README.md
+++ b/README.md
@@ -2,23 +2,30 @@ Casbin
====
[](https://goreportcard.com/report/github.com/casbin/casbin)
-[](https://travis-ci.org/casbin/casbin)
+[](https://github.com/casbin/casbin/actions/workflows/default.yml)
[](https://coveralls.io/github/casbin/casbin?branch=master)
-[](https://godoc.org/github.com/casbin/casbin)
+[](https://pkg.go.dev/github.com/casbin/casbin/v2)
[](https://github.com/casbin/casbin/releases/latest)
-[](https://gitter.im/casbin/lobby)
-[](http://www.patreon.com/yangluo)
-[](https://sourcegraph.com/github.com/casbin/casbin?badge)
+[](https://discord.gg/S5UjpzGZjN)
+[](https://sourcegraph.com/github.com/casbin/casbin?badge)
-**News**: Casbin is also started to port to Java ([jCasbin](https://github.com/casbin/jcasbin)) and PHP ([PHP-Casbin](https://github.com/sstutz/php-casbin)), contribution is welcomed.
+**News**: still worry about how to write the correct Casbin policy? ``Casbin online editor`` is coming to help! Try it at: https://casbin.org/editor/

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).
-### Supported by Auth0
+## All the languages supported by Casbin:
-If you want to easily add authentication and authorization to your Go projects, feel free to check out Auth0's Go SDK and free plan at [auth0.com/overview](https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=casbin&utm_content=auth)
+| [](https://github.com/casbin/casbin) | [](https://github.com/casbin/jcasbin) | [](https://github.com/casbin/node-casbin) | [](https://github.com/php-casbin/php-casbin) |
+|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
+| [Casbin](https://github.com/casbin/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 |
+
+| [](https://github.com/casbin/pycasbin) | [](https://github.com/casbin-net/Casbin.NET) | [](https://github.com/casbin/casbin-cpp) | [](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
@@ -27,16 +34,16 @@ If you want to easily add authentication and authorization to your Go projects,
- [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)
-- [Multi-threading](#multi-threading)
- [Benchmarks](#benchmarks)
- [Examples](#examples)
-- [How to use Casbin as a service?](#how-to-use-casbin-as-a-service)
+- [Middlewares](#middlewares)
- [Our adopters](#our-adopters)
## Supported models
@@ -75,6 +82,7 @@ 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:
@@ -89,6 +97,27 @@ 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 Casbin does:
@@ -96,40 +125,41 @@ What 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 explict permissions.
+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 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 Casbin is not designed as a password container. However, Casbin stores the user-role mapping for the RBAC scenario.
+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 Casbin is not designed as a password container. However, Casbin stores the user-role mapping for the RBAC scenario.
## Installation
```
-go get github.com/casbin/casbin
+go get github.com/casbin/casbin/v3
```
## Documentation
-For documentation, please see: [Our Wiki](https://github.com/casbin/casbin/wiki)
+https://casbin.org/docs/overview
+
+## Online editor
+
+You can also use the online editor (https://casbin.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
-- [Basic Role-Based HTTP Authorization in Go with Casbin](https://zupzup.org/casbin-http-role-auth) (or [Chinese translation](https://studygolang.com/articles/12323))
-- [Using Casbin with Beego: 1. Get started and test (in Chinese)](http://blog.csdn.net/hotqin888/article/details/78460385)
-- [Using Casbin with Beego: 2. Policy storage (in Chinese)](http://blog.csdn.net/hotqin888/article/details/78571240)
-- [Using Casbin with Beego: 3. Policy query (in Chinese)](http://blog.csdn.net/hotqin888/article/details/78992250)
+https://casbin.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")
+ 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 [Persistence](#persistence) section for details.
+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:
@@ -138,7 +168,7 @@ Note: you can also initialize an enforcer with policy in DB instead of file, see
obj := "data1" // the resource that is going to be accessed.
act := "read" // the operation that the user performs on the resource.
- if e.Enforce(sub, obj, act) == true {
+ if res, _ := e.Enforce(sub, obj, act); res {
// permit alice to read data1
} else {
// deny the request, show an error
@@ -148,199 +178,83 @@ Note: you can also initialize an enforcer with policy in DB instead of file, see
3. Besides the static policy file, 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.GetRoles("alice")
+ roles, _ := e.GetImplicitRolesForUser(sub)
```
See [Policy management APIs](#policy-management) for more usage.
-4. Please refer to the ``_test.go`` files for more usage.
-
## Policy management
Casbin provides two sets of APIs to manage permissions:
-- [Management API](https://github.com/casbin/casbin/blob/master/management_api.go): the primitive API that provides full support for Casbin policy management. See [here](https://github.com/casbin/casbin/blob/master/management_api_test.go) for examples.
-- [RBAC API](https://github.com/casbin/casbin/blob/master/rbac_api.go): 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. See [here](https://github.com/casbin/casbin/blob/master/rbac_api_test.go) for examples.
+- [Management API](https://casbin.org/docs/management-api): the primitive API that provides full support for Casbin policy management.
+- [RBAC API](https://casbin.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 for model management and policy management:
+We also provide a [web-based UI](https://casbin.org/docs/admin-portal) for model management and policy management:
-
+
-
+
## Policy persistence
-In Casbin, the policy storage is implemented as an adapter (aka middleware for Casbin). To keep light-weight, we don't put adapter code in the main library (except the default file adapter). A complete list of Casbin adapters is provided as below. Any 3rd-party contribution on a new adapter is welcomed, please inform us and I will put it in this list:)
-
-Adapter | Type | Author | Description
-----|------|----|----
-[File Adapter (built-in)](https://github.com/casbin/casbin/wiki/Policy-persistence#file-adapter) | File | Casbin | Persistence for [.CSV (Comma-Separated Values)](https://en.wikipedia.org/wiki/Comma-separated_values) files
-[Xorm Adapter](https://github.com/casbin/xorm-adapter) | ORM | Casbin | MySQL, PostgreSQL, TiDB, SQLite, SQL Server, Oracle are supported by [Xorm](https://github.com/go-xorm/xorm/)
-[Gorm Adapter](https://github.com/casbin/gorm-adapter) | ORM | Casbin | MySQL, PostgreSQL, Sqlite3, SQL Server are supported by [Gorm](https://github.com/jinzhu/gorm/)
-[Beego ORM Adapter](https://github.com/casbin/beego-orm-adapter) | ORM | Casbin | MySQL, PostgreSQL, Sqlite3 are supported by [Beego ORM](https://beego.me/docs/mvc/model/overview.md)
-[MongoDB Adapter](https://github.com/casbin/mongodb-adapter) | NoSQL | Casbin | Persistence for [MongoDB](https://www.mongodb.com)
-[Cassandra Adapter](https://github.com/casbin/cassandra-adapter) | NoSQL | Casbin | Persistence for [Apache Cassandra DB](http://cassandra.apache.org)
-[Consul Adapter](https://github.com/ankitm123/consul-adapter) | KV store | [@ankitm123](https://github.com/ankitm123) | Persistence for [HashiCorp Consul](https://www.consul.io/)
-[Redis Adapter](https://github.com/casbin/redis-adapter) | KV store | Casbin | Persistence for [Redis](https://redis.io/)
-[Protobuf Adapter](https://github.com/casbin/protobuf-adapter) | Stream | Casbin | Persistence for [Google Protocol Buffers](https://developers.google.com/protocol-buffers/)
-[JSON Adapter](https://github.com/casbin/json-adapter) | String | Casbin | Persistence for [JSON](https://www.json.org/)
-[String Adapter](https://github.com/qiangmzsx/string-adapter) | String | [@qiangmzsx](https://github.com/qiangmzsx) | Persistence for String
-[RQLite Adapter](https://github.com/edomosystems/rqlite-adapter) | SQL | [EDOMO Systems](https://github.com/edomosystems) | Persistence for [RQLite](https://github.com/rqlite/rqlite/)
-[PostgreSQL Adapter](https://github.com/going/casbin-postgres-adapter) | SQL | [Going](https://github.com/going) | Persistence for [PostgreSQL](https://www.postgresql.org/)
-[RethinkDB Adapter](https://github.com/adityapandey9/rethinkdb-adapter) | NoSQL | [@adityapandey9](https://github.com/adityapandey9) | Persistence for [RethinkDB](https://rethinkdb.com/)
-[DynamoDB Adapter](https://github.com/HOOQTV/dynacasbin) | NoSQL | [HOOQ](https://github.com/HOOQTV) | Persistence for [Amazon DynamoDB](https://aws.amazon.com/dynamodb/)
-[Minio/AWS S3 Adapter](https://github.com/Soluto/casbin-minio-adapter) | Object storage | [Soluto](https://github.com/Soluto) | Persistence for [Minio](https://github.com/minio/minio) and [Amazon S3](https://aws.amazon.com/s3/)
-[Bolt Adapter](https://github.com/wirepair/bolt-adapter) | KV store | [@wirepair](https://github.com/wirepair) | Persistence for [Bolt](https://github.com/boltdb/bolt)
-
-
-For details of adapters, please refer to the documentation: https://github.com/casbin/casbin/wiki/Policy-persistence
-
-## Policy enforcement at scale
-
-Some adapters support filtered policy management. This means that the policy loaded by Casbin is a subset of the policy in storage based on a given filter. This allows for efficient policy enforcement in large, multi-tenant environments when parsing the entire policy becomes a performance bottleneck.
-
-To use filtered policies with a supported adapter, simply call the `LoadFilteredPolicy` method. The valid format for the filter parameter depends on the adapter used. To prevent accidental data loss, the `SavePolicy` method is disabled when a filtered policy is loaded.
-
-For example, the following code snippet uses the built-in filtered file adapter and the RBAC model with domains. In this case, the filter limits the policy to a single domain. Any policy lines for domains other than `"domain1"` are omitted from the loaded policy:
-
-```go
-import (
- "github.com/casbin/casbin"
-)
-
-enforcer := casbin.NewEnforcer()
-
-adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
-enforcer.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)
-
-filter := &fileadapter.Filter{
- P: []string{"", "domain1"},
- G: []string{"", "", "domain1"},
-}
-enforcer.LoadFilteredPolicy(filter)
-
-// The loaded policy now only contains the entries pertaining to "domain1".
-```
+https://casbin.org/docs/adapters
## Policy consistence between multiple nodes
-We support to use distributed messaging systems like [etcd](https://github.com/coreos/etcd) to keep consistence between multiple Casbin enforcer instances. So our users can concurrently use multiple Casbin enforcers to handle large number of permission checking requests.
-
-Similar to policy storage adapters, we don't put watcher code in the main library. Any support for a new messaging system should be implemented as a watcher. A complete list of Casbin watchers is provided as below. Any 3rd-party contribution on a new watcher is welcomed, please inform us and I will put it in this list:)
-
-Watcher | Type | Author | Description
-----|------|----|----
-[Etcd Watcher](https://github.com/casbin/etcd-watcher) | KV store | Casbin | Watcher for [etcd](https://github.com/coreos/etcd)
-[NATS Watcher](https://github.com/Soluto/casbin-nats-watcher) | Messaging system | [Soluto](https://github.com/Soluto) | Watcher for [NATS](https://nats.io/)
+https://casbin.org/docs/watchers
## Role manager
-The role manager is used to manage the RBAC role hierarchy (user-role mapping) in Casbin. A role manager can retrieve the role data from Casbin policy rules or external sources such as LDAP, Okta, Auth0, Azure AD, etc. We support different implementations of a role manager. To keep light-weight, we don't put role manager code in the main library (except the default role manager). A complete list of Casbin role managers is provided as below. Any 3rd-party contribution on a new role manager is welcomed, please inform us and I will put it in this list:)
+https://casbin.org/docs/role-managers
-Role manager | Author | Description
-----|----|----
-[Default Role Manager (built-in)](https://github.com/casbin/casbin/blob/master/rbac/default-role-manager/role_manager.go) | Casbin | Supports role hierarchy stored in Casbin policy
-[Session Role Manager](https://github.com/casbin/session-role-manager) | [EDOMO Systems](https://github.com/edomosystems) | Supports role hierarchy stored in Casbin policy, with time-range-based sessions
-[Okta Role Manager](https://github.com/casbin/okta-role-manager) | Casbin | Supports role hierarchy stored in [Okta](https://www.okta.com/)
-[Auth0 Role Manager](https://github.com/casbin/auth0-role-manager) | Casbin | Supports role hierarchy stored in [Auth0](https://www.okta.com/)'s [Authorization Extension](https://auth0.com/docs/extensions/authorization-extension/v2)
+## Benchmarks
-For developers: all role managers must implement the [RoleManager](https://github.com/casbin/casbin/blob/master/rbac/role_manager.go) interface. [Session Role Manager](https://github.com/casbin/session-role-manager) can be used as a reference implementation.
+https://casbin.org/docs/benchmark
-## Multi-threading
+## Examples
-If you use Casbin in a multi-threading manner, you can use the synchronized wrapper of the Casbin enforcer: https://github.com/casbin/casbin/blob/master/enforcer_synced.go.
+| Model | Model file | Policy file |
+|---------------------------|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
+| ACL | [basic_model.conf](https://github.com/casbin/casbin/blob/master/examples/basic_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin/blob/master/examples/basic_policy.csv) |
+| ACL with superuser | [basic_model_with_root.conf](https://github.com/casbin/casbin/blob/master/examples/basic_with_root_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin/blob/master/examples/basic_policy.csv) |
+| ACL without users | [basic_model_without_users.conf](https://github.com/casbin/casbin/blob/master/examples/basic_without_users_model.conf) | [basic_policy_without_users.csv](https://github.com/casbin/casbin/blob/master/examples/basic_without_users_policy.csv) |
+| ACL without resources | [basic_model_without_resources.conf](https://github.com/casbin/casbin/blob/master/examples/basic_without_resources_model.conf) | [basic_policy_without_resources.csv](https://github.com/casbin/casbin/blob/master/examples/basic_without_resources_policy.csv) |
+| RBAC | [rbac_model.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_model.conf) | [rbac_policy.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_policy.csv) |
+| RBAC with resource roles | [rbac_model_with_resource_roles.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_resource_roles_model.conf) | [rbac_policy_with_resource_roles.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_resource_roles_policy.csv) |
+| RBAC with domains/tenants | [rbac_model_with_domains.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_model.conf) | [rbac_policy_with_domains.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_policy.csv) |
+| ABAC | [abac_model.conf](https://github.com/casbin/casbin/blob/master/examples/abac_model.conf) | N/A |
+| RESTful | [keymatch_model.conf](https://github.com/casbin/casbin/blob/master/examples/keymatch_model.conf) | [keymatch_policy.csv](https://github.com/casbin/casbin/blob/master/examples/keymatch_policy.csv) |
+| Deny-override | [rbac_model_with_deny.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_deny_model.conf) | [rbac_policy_with_deny.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_deny_policy.csv) |
+| Priority | [priority_model.conf](https://github.com/casbin/casbin/blob/master/examples/priority_model.conf) | [priority_policy.csv](https://github.com/casbin/casbin/blob/master/examples/priority_policy.csv) |
-It also supports the ``AutoLoad`` feature, which means the Casbin enforcer will automatically load the latest policy rules from DB if it has changed. Call ``StartAutoLoadPolicy()`` to start automatically loading policy periodically and call ``StopAutoLoadPolicy()`` to stop it.
+## Middlewares
-## Benchmarks
+Authz middlewares for web frameworks: https://casbin.org/docs/middlewares
-The overhead of policy enforcement is benchmarked in [model_b_test.go](https://github.com/casbin/casbin/blob/master/model_b_test.go). The testbed is:
+## Our adopters
-```
-Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz, 2601 Mhz, 4 Core(s), 8 Logical Processor(s)
-```
+https://casbin.org/docs/adopters
-The benchmarking result of ``go test -bench=. -benchmem`` is as follows (op = an ``Enforce()`` call, ms = millisecond, KB = kilo bytes):
-
-Test case | Size | Time overhead | Memory overhead
-----|------|------|----
-ACL | 2 rules (2 users) | 0.015493 ms/op | 5.649 KB
-RBAC | 5 rules (2 users, 1 role) | 0.021738 ms/op | 7.522 KB
-RBAC (small) | 1100 rules (1000 users, 100 roles) | 0.164309 ms/op | 80.620 KB
-RBAC (medium) | 11000 rules (10000 users, 1000 roles) | 2.258262 ms/op | 765.152 KB
-RBAC (large) | 110000 rules (100000 users, 10000 roles) | 23.916776 ms/op | 7.606 MB
-RBAC with resource roles | 6 rules (2 users, 2 roles) | 0.021146 ms/op | 7.906 KB
-RBAC with domains/tenants | 6 rules (2 users, 1 role, 2 domains) | 0.032696 ms/op | 10.755 KB
-ABAC | 0 rule (0 user) | 0.007510 ms/op | 2.328 KB
-RESTful | 5 rules (3 users) | 0.045398 ms/op | 91.774 KB
-Deny-override | 6 rules (2 users, 1 role) | 0.023281 ms/op | 8.370 KB
-Priority | 9 rules (2 users, 2 roles) | 0.016389 ms/op | 5.313 KB
+## How to Contribute
-## Examples
+Please read the [contributing guide](CONTRIBUTING.md).
-Model | Model file | Policy file
-----|------|----
-ACL | [basic_model.conf](https://github.com/casbin/casbin/blob/master/examples/basic_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin/blob/master/examples/basic_policy.csv)
-ACL with superuser | [basic_model_with_root.conf](https://github.com/casbin/casbin/blob/master/examples/basic_with_root_model.conf) | [basic_policy.csv](https://github.com/casbin/casbin/blob/master/examples/basic_policy.csv)
-ACL without users | [basic_model_without_users.conf](https://github.com/casbin/casbin/blob/master/examples/basic_without_users_model.conf) | [basic_policy_without_users.csv](https://github.com/casbin/casbin/blob/master/examples/basic_without_users_policy.csv)
-ACL without resources | [basic_model_without_resources.conf](https://github.com/casbin/casbin/blob/master/examples/basic_without_resources_model.conf) | [basic_policy_without_resources.csv](https://github.com/casbin/casbin/blob/master/examples/basic_without_resources_policy.csv)
-RBAC | [rbac_model.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_model.conf) | [rbac_policy.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_policy.csv)
-RBAC with resource roles | [rbac_model_with_resource_roles.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_resource_roles_model.conf) | [rbac_policy_with_resource_roles.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_resource_roles_policy.csv)
-RBAC with domains/tenants | [rbac_model_with_domains.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_model.conf) | [rbac_policy_with_domains.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_domains_policy.csv)
-ABAC | [abac_model.conf](https://github.com/casbin/casbin/blob/master/examples/abac_model.conf) | N/A
-RESTful | [keymatch_model.conf](https://github.com/casbin/casbin/blob/master/examples/keymatch_model.conf) | [keymatch_policy.csv](https://github.com/casbin/casbin/blob/master/examples/keymatch_policy.csv)
-Deny-override | [rbac_model_with_deny.conf](https://github.com/casbin/casbin/blob/master/examples/rbac_with_deny_model.conf) | [rbac_policy_with_deny.csv](https://github.com/casbin/casbin/blob/master/examples/rbac_with_deny_policy.csv)
-Priority | [priority_model.conf](https://github.com/casbin/casbin/blob/master/examples/priority_model.conf) | [priority_policy.csv](https://github.com/casbin/casbin/blob/master/examples/priority_policy.csv)
-
-## How to use Casbin as a service?
-
-- [Go-Simple-API-Gateway](https://github.com/Soontao/go-simple-api-gateway): A simple API gateway written by golang, supports for authentication and authorization
-- [Casbin Server](https://github.com/casbin/casbin-server): Casbin as a Service via RESTful, only exposed permission checking API
-- [middleware-acl](https://github.com/luk4z7/middleware-acl): RESTful access control middleware based on Casbin
+## Contributors
-## Our adopters
+This project exists thanks to all the people who contribute.
+
+
+## Star History
-### Web frameworks
-
-- [Beego](https://github.com/astaxie/beego): An open-source, high-performance web framework for Go, via built-in plugin: [plugins/authz](https://github.com/astaxie/beego/blob/master/plugins/authz)
-- [Caddy](https://github.com/mholt/caddy): Fast, cross-platform HTTP/2 web server with automatic HTTPS, via plugin: [caddy-authz](https://github.com/casbin/caddy-authz)
-- [Gin](https://github.com/gin-gonic/gin): A HTTP web framework featuring a Martini-like API with much better performance, via plugin: [authz](https://github.com/gin-contrib/authz)
-- [Revel](https://github.com/revel/revel): A high productivity, full-stack web framework for the Go language, via plugin: [auth/casbin](https://github.com/revel/modules/tree/master/auth/casbin)
-- [Echo](https://github.com/labstack/echo): High performance, minimalist Go web framework, via plugin: [echo-authz](https://github.com/labstack/echo-contrib/tree/master/casbin) (thanks to [@xqbumu](https://github.com/xqbumu))
-- [Iris](https://github.com/kataras/iris): The fastest web framework for Go in (THIS) Earth. HTTP/2 Ready-To-GO, via plugin: [casbin](https://github.com/iris-contrib/middleware/tree/master/casbin) (thanks to [@hiveminded](https://github.com/hiveminded))
-- [Negroni](https://github.com/urfave/negroni): Idiomatic HTTP Middleware for Golang, via plugin: [negroni-authz](https://github.com/casbin/negroni-authz)
-- [Tango](https://github.com/lunny/tango): Micro & pluggable web framework for Go, via plugin: [authz](https://github.com/tango-contrib/authz)
-- [Chi](https://github.com/pressly/chi): A lightweight, idiomatic and composable router for building HTTP services, via plugin: [chi-authz](https://github.com/casbin/chi-authz)
-- [Macaron](https://github.com/go-macaron/macaron): A high productive and modular web framework in Go, via plugin: [authz](https://github.com/go-macaron/authz)
-- [DotWeb](https://github.com/devfeel/dotweb): Simple and easy go web micro framework, via plugin: [authz](https://github.com/devfeel/middleware/tree/master/authz)
-- [Baa](https://github.com/go-baa/baa): An express Go web framework with routing, middleware, dependency injection and http context, via plugin: [authz](https://github.com/baa-middleware/authz)
-
-### Others
-
-- [Intel RMD](https://github.com/intel/rmd): Intel's resource management daemon, via direct integration, see: [model](https://github.com/intel/rmd/blob/master/etc/rmd/acl/url/model.conf), [policy rules](https://github.com/intel/rmd/blob/master/etc/rmd/acl/url/policy.csv)
-- [VMware Dispatch](https://github.com/vmware/dispatch): A framework for deploying and managing serverless style applications, via direct integration, see: [model (in code)](https://github.com/vmware/dispatch/blob/master/pkg/identity-manager/handlers.go#L46-L55), [policy rules (in code)](https://github.com/vmware/dispatch/blob/master/pkg/identity-manager/handlers_test.go#L35-L45)
-- [Docker](https://github.com/docker/docker): The world's leading software container platform, via plugin: [casbin-authz-plugin](https://github.com/casbin/casbin-authz-plugin) ([recommended by Docker](https://docs.docker.com/engine/extend/legacy_plugins/#authorization-plugins))
-- [Gobis](https://github.com/orange-cloudfoundry/gobis): [Orange](https://github.com/orange-cloudfoundry)'s lightweight API Gateway written in go, via plugin: [casbin](https://github.com/orange-cloudfoundry/gobis-middlewares/tree/master/casbin), see [model (in code)](https://github.com/orange-cloudfoundry/gobis-middlewares/blob/master/casbin/model.go#L52-L65), [policy rules (from request)](https://github.com/orange-cloudfoundry/gobis-middlewares/blob/master/casbin/adapter.go#L46-L64)
-- [Zenpress](https://github.com/insionng/zenpress): A CMS system written in Golang, via direct integration, see: [model](https://github.com/insionng/zenpress/blob/master/content/config/rbac_model.conf), [policy rules (in Gorm)](https://github.com/insionng/zenpress/blob/master/model/user.go#L53-L77)
-- [EngineerCMS](https://github.com/3xxx/EngineerCMS): A CMS to manage knowledge for engineers, via direct integration, see: [model](https://github.com/3xxx/EngineerCMS/blob/master/conf/rbac_model.conf), [policy rules (in SQLite)](https://github.com/3xxx/EngineerCMS/blob/master/database/engineer.db)
-- [Cyber Auth API](https://github.com/CyberlifeCN/cyber-auth-api): A Golang authentication API project, via direct integration, see: [model](https://github.com/CyberlifeCN/cyber-auth-api/blob/master/conf/authz_model.conf), [policy rules](https://github.com/CyberlifeCN/cyber-auth-api/blob/master/conf/authz_policy.csv)
-- [IRIS Community](https://github.com/irisnet/iris-community): Website for IRIS Community Activities, via direct integration, see: [model](https://github.com/irisnet/iris-community/blob/master/authz/authz_model.conf), [policy rules](https://github.com/irisnet/iris-community/blob/master/authz/authz_policy.csv)
-- [Metadata DB](https://github.com/Bnei-Baruch/mdb): BB archive metadata database, via direct integration, see: [model](https://github.com/Bnei-Baruch/mdb/blob/master/data/permissions_model.conf), [policy rules](https://github.com/Bnei-Baruch/mdb/blob/master/data/permissions_policy.csv)
+[](https://star-history.com/#casbin/casbin&Date)
## License
-This project is licensed under the [Apache 2.0 license](https://github.com/casbin/casbin/blob/master/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/casbin/casbin/issues
-- hsluoyz@gmail.com
-- Tencent QQ group: [546057381](//shang.qq.com/wpa/qunwpa?idkey=8ac8b91fc97ace3d383d0035f7aa06f7d670fd8e8d4837347354a31c18fac885)
-
-## Donation
-
-[](http://www.patreon.com/yangluo)
-
-
-
+- https://discord.gg/S5UjpzGZjN
diff --git a/abac_test.go b/abac_test.go
new file mode 100644
index 000000000..56e0998d1
--- /dev/null
+++ b/abac_test.go
@@ -0,0 +1,198 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..78cef5571
--- /dev/null
+++ b/ai_api.go
@@ -0,0 +1,221 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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")
+ sb.WriteString(fmt.Sprintf("Subject: %v\n", rvals[0]))
+ if len(rvals) > 1 {
+ sb.WriteString(fmt.Sprintf("Object: %v\n", rvals[1]))
+ }
+ if len(rvals) > 2 {
+ sb.WriteString(fmt.Sprintf("Action: %v\n", rvals[2]))
+ }
+ sb.WriteString(fmt.Sprintf("\nEnforcement Result: %v\n", result))
+
+ // Add matched rules
+ if len(matchedRules) > 0 {
+ sb.WriteString("\nMatched Policy Rules:\n")
+ for _, rule := range matchedRules {
+ sb.WriteString(fmt.Sprintf("- %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 {
+ sb.WriteString(fmt.Sprintf("Matcher (%s): %s\n", key, ast.Value))
+ }
+ }
+ if eff, ok := e.model["e"]; ok {
+ for key, ast := range eff {
+ sb.WriteString(fmt.Sprintf("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 {
+ sb.WriteString(fmt.Sprintf("- %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 000000000..4ff0b4e98
--- /dev/null
+++ b/ai_api_test.go
@@ -0,0 +1,250 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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/asf.yaml b/asf.yaml
new file mode 100644
index 000000000..00cedfcb4
--- /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/biba_test.go b/biba_test.go
new file mode 100644
index 000000000..c0b864f2c
--- /dev/null
+++ b/biba_test.go
@@ -0,0 +1,44 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..7211505f5
--- /dev/null
+++ b/blp_test.go
@@ -0,0 +1,50 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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/config/config.go b/config/config.go
index 07466f4b9..57d40d849 100644
--- a/config/config.go
+++ b/config/config.go
@@ -23,15 +23,20 @@ import (
"os"
"strconv"
"strings"
- "sync"
)
var (
- DEFAULT_SECTION = "default"
- DEFAULT_COMMENT = []byte{'#'}
+ // DEFAULT_SECTION specifies the name of a section if no name provided.
+ DEFAULT_SECTION = "default"
+ // DEFAULT_COMMENT defines what character(s) indicate a comment `#`.
+ DEFAULT_COMMENT = []byte{'#'}
+ // DEFAULT_COMMENT_SEM defines what alternate character(s) indicate a comment `;`.
DEFAULT_COMMENT_SEM = []byte{';'}
+ // DEFAULT_MULTI_LINE_SEPARATOR defines what character indicates a multi-line content.
+ DEFAULT_MULTI_LINE_SEPARATOR = []byte{'\\'}
)
+// ConfigInterface defines the behavior of a Config implementation.
type ConfigInterface interface {
String(key string) string
Strings(key string) []string
@@ -42,9 +47,8 @@ type ConfigInterface interface {
Set(key string, value string) error
}
+// Config represents an implementation of the ConfigInterface.
type Config struct {
- // map is not safe.
- sync.RWMutex
// Section:key=value
data map[string]map[string]string
}
@@ -84,75 +88,130 @@ func (c *Config) AddConfig(section string, option string, value string) bool {
}
func (c *Config) parse(fname string) (err error) {
- c.Lock()
f, err := os.Open(fname)
if err != nil {
return err
}
- defer c.Unlock()
defer f.Close()
buf := bufio.NewReader(f)
return c.parseBuffer(buf)
}
-func (c *Config) parseBuffer(buf *bufio.Reader) (err error) {
+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 bytes.Equal(line, []byte{}) {
- continue
} else if err != nil {
return err
}
line = bytes.TrimSpace(line)
switch {
- case bytes.HasPrefix(line, DEFAULT_COMMENT):
- continue
- case bytes.HasPrefix(line, DEFAULT_COMMENT_SEM):
+ 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:
- optionVal := bytes.SplitN(line, []byte{'='}, 2)
- if len(optionVal) != 2 {
- return fmt.Errorf("parse the content error : line %d , %s = ? ", lineNum, optionVal[0])
+ 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
}
- option := bytes.TrimSpace(optionVal[0])
- value := bytes.TrimSpace(optionVal[1])
- c.AddConfig(section, string(option), string(value))
}
}
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 == "" {
@@ -161,9 +220,8 @@ func (c *Config) Strings(key string) []string {
return strings.Split(v, ",")
}
+// Set sets the value for the specific key in the Config.
func (c *Config) Set(key string, value string) error {
- c.Lock()
- defer c.Unlock()
if len(key) == 0 {
return errors.New("key is empty")
}
@@ -185,7 +243,7 @@ func (c *Config) Set(key string, value string) error {
return nil
}
-// section.key or key
+// section.key or key.
func (c *Config) get(key string) string {
var (
section string
diff --git a/config/config_test.go b/config/config_test.go
index 957aeed44..8431ae10c 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -19,14 +19,14 @@ import (
)
func TestGet(t *testing.T) {
- config, err := NewConfig("testdata/testini.ini")
- if err != nil {
- t.Errorf("Configuration file loading failed, err:%v", err.Error())
- t.Fatalf("err: %v", err)
+ 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 != true {
+ 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)
}
@@ -34,7 +34,7 @@ func TestGet(t *testing.T) {
t.Errorf("Get failure: expected different value for url (expected: [%#v] got: [%#v])", "act.wiki", v)
}
- // reids::key test
+ // 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)
}
@@ -44,7 +44,12 @@ func TestGet(t *testing.T) {
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)
@@ -55,23 +60,31 @@ func TestGet(t *testing.T) {
t.Fatalf("err: %v", err)
}
- // other::key test
- if v := config.String("other::name"); v != "ATCčŖåØåęµčÆ^-^&($#ā¦ā¦#" {
- t.Errorf("Get failure: expected different value for other::name (expected: [%#v] got: [%#v])", "ATCčŖåØåęµčÆ^-^&($#ā¦ā¦#", 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)
}
- if v := config.String("other::key1"); v != "test key" {
- t.Errorf("Get failure: expected different value for other::key1 (expected: [%#v] got: [%#v])", "test key", v)
- t.Fatalf("err: %v", err)
+ _ = 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)
}
- config.Set("other::key1", "new test key")
+ 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("other::key1"); v != "new test key" {
- t.Errorf("Get failure: expected different value for other::key1 (expected: [%#v] got: [%#v])", "new test key", v)
- t.Fatalf("err: %v", err)
+ 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)
}
- config.Set("other::key1", "test key")
+ 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
index 10d4e637c..cbb9bab5b 100644
--- a/config/testdata/testini.ini
+++ b/config/testdata/testini.ini
@@ -13,8 +13,8 @@ mysql.dev.user = root
mysql.dev.pass = 123456
mysql.dev.db = test
-mysql.master.host = 10.0.0.1
-mysql.master.user = root
+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
@@ -23,7 +23,25 @@ mysql.master.db = act
math.i64 = 64
math.f64 = 64.1
-; other config
-[other]
-name = ATCčŖåØåęµčÆ^-^&($#ā¦ā¦#
-key1 = test key
+# 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 000000000..4140ecf3f
--- /dev/null
+++ b/constant/constants.go
@@ -0,0 +1,31 @@
+// Copyright 2022 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..4061c68d9
--- /dev/null
+++ b/constraint_test.go
@@ -0,0 +1,333 @@
+// Copyright 2024 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..d2fcc8d78
--- /dev/null
+++ b/detector/default_detector.go
@@ -0,0 +1,150 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..8f4ce558d
--- /dev/null
+++ b/detector/default_detector_test.go
@@ -0,0 +1,345 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..497123c95
--- /dev/null
+++ b/detector/detector.go
@@ -0,0 +1,25 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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/effect/default_effector.go b/effect/default_effector.go
deleted file mode 100644
index 7368e19f2..000000000
--- a/effect/default_effector.go
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2018 The casbin Authors. All Rights Reserved.
-//
-// 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.
-
-package effect
-
-import "github.com/pkg/errors"
-
-// 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, results []float64) (bool, error) {
- result := false
- if expr == "some(where (p_eft == allow))" {
- result = false
- for _, eft := range effects {
- if eft == Allow {
- result = true
- break
- }
- }
- } else if expr == "!some(where (p_eft == deny))" {
- result = true
- for _, eft := range effects {
- if eft == Deny {
- result = false
- break
- }
- }
- } else if expr == "some(where (p_eft == allow)) && !some(where (p_eft == deny))" {
- result = false
- for _, eft := range effects {
- if eft == Allow {
- result = true
- } else if eft == Deny {
- result = false
- break
- }
- }
- } else if expr == "priority(p_eft) || deny" {
- result = false
- for _, eft := range effects {
- if eft != Indeterminate {
- if eft == Allow {
- result = true
- } else {
- result = false
- }
- break
- }
- }
- } else {
- return false, errors.New("unsupported effect")
- }
-
- return result, nil
-}
diff --git a/effector/default_effector.go b/effector/default_effector.go
new file mode 100644
index 000000000..fca8912ed
--- /dev/null
+++ b/effector/default_effector.go
@@ -0,0 +1,109 @@
+// Copyright 2018 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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/effect/effector.go b/effector/effector.go
similarity index 85%
rename from effect/effector.go
rename to effector/effector.go
index 52271b3f2..49b84c3e1 100644
--- a/effect/effector.go
+++ b/effector/effector.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package effect
+package effector //nolint:cyclop // TODO
// Effect is the result for a policy rule.
type Effect int
@@ -27,5 +27,5 @@ const (
// 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, results []float64) (bool, error)
+ MergeEffects(expr string, effects []Effect, matches []float64, policyIndex int, policyLength int) (Effect, int, error)
}
diff --git a/enforcer.go b/enforcer.go
index 4d21a2907..a6bf1740a 100644
--- a/enforcer.go
+++ b/enforcer.go
@@ -17,15 +17,21 @@ package casbin
import (
"errors"
"fmt"
+ "runtime/debug"
+ "strings"
+ "sync"
- "github.com/Knetic/govaluate"
- "github.com/casbin/casbin/effect"
- "github.com/casbin/casbin/model"
- "github.com/casbin/casbin/persist"
- "github.com/casbin/casbin/persist/file-adapter"
- "github.com/casbin/casbin/rbac"
- "github.com/casbin/casbin/rbac/default-role-manager"
- "github.com/casbin/casbin/util"
+ "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.
@@ -33,89 +39,129 @@ type Enforcer struct {
modelPath string
model model.Model
fm model.FunctionMap
- eft effect.Effector
+ 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
- adapter persist.Adapter
- watcher persist.Watcher
- rm rbac.RoleManager
+ 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
+}
- enabled bool
- autoSave bool
- autoBuildRoleLinks bool
+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.conf")
+//
+// 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 {
+//
+// 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{}
- e.rm = defaultrolemanager.NewRoleManager(10)
- e.eft = effect.NewDefaultEffector()
parsedParamLen := 0
- if len(params) >= 1 {
- enableLog, ok := params[len(params)-1].(bool)
- if ok {
- e.EnableLog(enableLog)
-
- parsedParamLen++
- }
- }
+ paramLen := len(params)
- if len(params)-parsedParamLen == 2 {
- switch params[0].(type) {
+ switch paramLen - parsedParamLen {
+ case 2:
+ switch p0 := params[0].(type) {
case string:
- switch params[1].(type) {
+ switch p1 := params[1].(type) {
case string:
- e.InitWithFile(params[0].(string), params[1].(string))
+ err := e.InitWithFile(p0, p1)
+ if err != nil {
+ return nil, err
+ }
default:
- e.InitWithAdapter(params[0].(string), params[1].(persist.Adapter))
+ err := e.InitWithAdapter(p0, p1.(persist.Adapter))
+ if err != nil {
+ return nil, err
+ }
}
default:
switch params[1].(type) {
case string:
- panic("Invalid parameters for enforcer.")
+ return nil, errors.New("invalid parameters for enforcer")
default:
- e.InitWithModelAndAdapter(params[0].(model.Model), params[1].(persist.Adapter))
+ err := e.InitWithModelAndAdapter(p0.(model.Model), params[1].(persist.Adapter))
+ if err != nil {
+ return nil, err
+ }
}
}
- } else if len(params)-parsedParamLen == 1 {
- switch params[0].(type) {
+ case 1:
+ switch p0 := params[0].(type) {
case string:
- e.InitWithFile(params[0].(string), "")
+ err := e.InitWithFile(p0, "")
+ if err != nil {
+ return nil, err
+ }
default:
- e.InitWithModelAndAdapter(params[0].(model.Model), nil)
+ err := e.InitWithModelAndAdapter(p0.(model.Model), nil)
+ if err != nil {
+ return nil, err
+ }
}
- } else if len(params)-parsedParamLen == 0 {
- e.InitWithFile("", "")
- } else {
- panic("Invalid parameters for enforcer.")
+ case 0:
+ return e, nil
+ default:
+ return nil, errors.New("invalid parameters for enforcer")
}
- return e
+ return e, nil
}
// InitWithFile initializes an enforcer with a model file and a policy file.
-func (e *Enforcer) InitWithFile(modelPath string, policyPath string) {
+func (e *Enforcer) InitWithFile(modelPath string, policyPath string) error {
a := fileadapter.NewAdapter(policyPath)
- e.InitWithAdapter(modelPath, a)
+ return e.InitWithAdapter(modelPath, a)
}
// InitWithAdapter initializes an enforcer with a database adapter.
-func (e *Enforcer) InitWithAdapter(modelPath string, adapter persist.Adapter) {
- m := NewModel(modelPath, "")
- e.InitWithModelAndAdapter(m, 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) {
+func (e *Enforcer) InitWithModelAndAdapter(m model.Model, adapter persist.Adapter) error {
e.adapter = adapter
- e.watcher = nil
e.model = m
e.model.PrintModel()
@@ -123,41 +169,53 @@ func (e *Enforcer) InitWithModelAndAdapter(m model.Model, adapter persist.Adapte
e.initialize()
- if e.adapter != nil {
- e.LoadPolicy()
+ // 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
-}
-
-// NewModel creates a model.
-func NewModel(text ...string) model.Model {
- m := make(model.Model)
+ e.autoNotifyWatcher = true
+ e.autoNotifyDispatcher = true
+ e.initRmMap()
- if len(text) == 2 {
- if text[0] != "" {
- m.LoadModel(text[0])
- }
- } else if len(text) == 1 {
- m.LoadModelFromText(text[0])
- } else if len(text) != 0 {
- panic("Invalid parameters for model.")
+ // Initialize detectors with default detector if not already set
+ if e.detectors == nil {
+ e.detectors = []detector.Detector{detector.NewDefaultDetector()}
}
-
- return m
}
// 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() {
- e.model = NewModel()
- e.model.LoadModel(e.modelPath)
+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.
@@ -169,6 +227,8 @@ func (e *Enforcer) GetModel() model.Model {
func (e *Enforcer) SetModel(m model.Model) {
e.model = m
e.fm = model.LoadFunctionMap()
+
+ e.initialize()
}
// GetAdapter gets the current adapter.
@@ -182,66 +242,278 @@ func (e *Enforcer) SetAdapter(adapter persist.Adapter) {
}
// SetWatcher sets the current watcher.
-func (e *Enforcer) SetWatcher(watcher persist.Watcher) {
+func (e *Enforcer) SetWatcher(watcher persist.Watcher) error {
e.watcher = watcher
- watcher.SetUpdateCallback(func(string) { e.LoadPolicy() })
+ 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.rm = rm
+ 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 effect.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 e.detectors == nil || 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 {
- e.model.ClearPolicy()
- err := e.adapter.LoadPolicy(e.model)
+ 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.model.PrintPolicy()
+ 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 {
- e.BuildRoleLinks()
+ 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
}
-// LoadFilteredPolicy reloads a filtered policy from file/database.
-func (e *Enforcer) LoadFilteredPolicy(filter interface{}) error {
- e.model.ClearPolicy()
+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 e.adapter.(type) {
+ switch adapter := e.adapter.(type) {
case persist.FilteredAdapter:
- filteredAdapter = e.adapter.(persist.FilteredAdapter)
+ filteredAdapter = adapter
default:
return errors.New("filtered policies are not supported by this adapter")
}
- err := filteredAdapter.LoadFilteredPolicy(e.model, filter)
- if err != nil {
+ 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 {
- e.BuildRoleLinks()
+ 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)
@@ -253,16 +525,86 @@ func (e *Enforcer) IsFiltered() bool {
// 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() {
- return errors.New("cannot save a filtered policy")
+ err := errors.New("cannot save a filtered policy")
+ e.onLogAfterEventWithError(logEntry, err)
+ return err
}
- err := e.adapter.SavePolicy(e.model)
- if err == nil {
- if e.watcher != nil {
- e.watcher.Update()
+ 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)
}
}
- return err
}
// EnableEnforce changes the enforcing state of Casbin, when Casbin is disabled, all access will be allowed by the Enforce() function.
@@ -270,9 +612,14 @@ func (e *Enforcer) EnableEnforce(enable bool) {
e.enabled = enable
}
-// EnableLog changes whether to print Casbin log to the standard output.
-func (e *Enforcer) EnableLog(enable bool) {
- util.EnableLog = 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.
@@ -285,133 +632,456 @@ 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
+}
+
// BuildRoleLinks manually rebuild the role inheritance relations.
-func (e *Enforcer) BuildRoleLinks() {
- e.rm.Clear()
- e.model.BuildRoleLinks(e.rm)
+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)
}
-// 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 {
- if !e.enabled {
- return true
+// 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)
+ }()
- functions := make(map[string]govaluate.ExpressionFunction)
- for key, function := range e.fm {
- functions[key] = function
+ if !e.enabled {
+ return true, nil
}
+
+ functions := e.fm.GetFunctions()
if _, ok := e.model["g"]; ok {
for key, ast := range e.model["g"] {
- rm := ast.RM
- functions[key] = util.GenerateGFunction(rm)
+ // 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)
+ }
+ if ast.CondRM != nil {
+ functions[key] = util.GenerateConditionalGFunction(ast.CondRM)
+ }
}
}
- expString := e.model["m"]["m"].Value
- expression, _ := govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
+ 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 policyEffects []effect.Effect
- var matcherResults []float64
- if policyLen := len(e.model["p"]["p"].Policy); policyLen != 0 {
- policyEffects = make([]effect.Effect, policyLen)
- matcherResults = make([]float64, policyLen)
+ 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)))
+ }
- for i, pvals := range e.model["p"]["p"].Policy {
- // util.LogPrint("Policy Rule: ", pvals)
+ 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
+ }
- parameters := make(map[string]interface{}, 8)
- for j, token := range e.model["r"]["r"].Tokens {
- parameters[token] = rvals[j]
+ 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
+ }
}
- for j, token := range e.model["p"]["p"].Tokens {
- parameters[token] = pvals[j]
+ }
+ }
+
+ 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)
}
- result, err := expression.Evaluate(parameters)
- // util.LogPrint("Result: ", result)
+ parameters.pVals = pvals
+
+ result, err := expression.Eval(parameters)
+ // log.LogPrint("Result: ", result)
if err != nil {
- policyEffects[i] = effect.Indeterminate
- panic(err)
- } else {
- switch result.(type) {
- case bool:
- if !result.(bool) {
- policyEffects[i] = effect.Indeterminate
- continue
- }
- case float64:
- if result.(float64) == 0 {
- policyEffects[i] = effect.Indeterminate
- continue
- } else {
- matcherResults[i] = result.(float64)
- }
- default:
- panic(errors.New("matcher result should be bool, int or float"))
+ return false, err
+ }
+
+ // set to no-match at first
+ matcherResults[policyIndex] = 0
+ switch result := result.(type) {
+ case bool:
+ if result {
+ matcherResults[policyIndex] = 1
}
- if eft, ok := parameters["p_eft"]; ok {
- if eft == "allow" {
- policyEffects[i] = effect.Allow
- } else if eft == "deny" {
- policyEffects[i] = effect.Deny
- } else {
- policyEffects[i] = effect.Indeterminate
- }
- } else {
- policyEffects[i] = effect.Allow
+ case float64:
+ if result != 0 {
+ matcherResults[policyIndex] = 1
}
+ default:
+ return false, errors.New("matcher result should be bool, int or float")
+ }
- if e.model["e"]["e"].Value == "priority(p_eft) || deny" {
- break
+ if j, ok := parameters.pTokens[pType+"_eft"]; ok {
+ eft := parameters.pVals[j]
+ if eft == "allow" {
+ policyEffects[policyIndex] = effector.Allow
+ } else if eft == "deny" {
+ policyEffects[policyIndex] = effector.Deny
+ } else {
+ 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 {
- policyEffects = make([]effect.Effect, 1)
+ 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
+ }
- parameters := make(map[string]interface{}, 8)
- for j, token := range e.model["r"]["r"].Tokens {
- parameters[token] = rvals[j]
+ 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
}
- for _, token := range e.model["p"]["p"].Tokens {
- parameters[token] = ""
+ }
+
+ if explains != nil {
+ if explainIndex != -1 && len(e.model["p"][pType].Policy) > explainIndex {
+ *explains = e.model["p"][pType].Policy[explainIndex]
}
+ }
+
+ // effect -> result
+ result := false
+ if effect == effector.Allow {
+ result = true
+ }
+
+ return result, nil
+}
- result, err := expression.Evaluate(parameters)
- // util.LogPrint("Result: ", result)
+func (e *Enforcer) getAndStoreMatcherExpression(hasEval bool, expString string, functions map[string]govaluate.ExpressionFunction) (*govaluate.EvaluableExpression, error) {
+ var expression *govaluate.EvaluableExpression
+ var err error
+ var cachedExpression, isPresent = e.matcherMap.Load(expString)
+ if !hasEval && isPresent {
+ expression = cachedExpression.(*govaluate.EvaluableExpression)
+ } else {
+ expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
if err != nil {
- policyEffects[0] = effect.Indeterminate
- panic(err)
- } else {
- if result.(bool) {
- policyEffects[0] = effect.Allow
- } else {
- policyEffects[0] = effect.Indeterminate
- }
+ return nil, err
}
+ e.matcherMap.Store(expString, expression)
}
+ return expression, nil
+}
- // util.LogPrint("Rule Results: ", policyEffects)
+// 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...)
+}
- result, err := e.eft.MergeEffects(e.model["e"]["e"].Value, policyEffects, matcherResults)
- if err != nil {
- panic(err)
+// 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
+}
- reqStr := "Request: "
- for i, rval := range rvals {
- if i != len(rvals)-1 {
- reqStr += fmt.Sprintf("%v, ", rval)
- } else {
- reqStr += fmt.Sprintf("%v", rval)
+// 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)
}
- reqStr += fmt.Sprintf(" ---> %t", result)
- util.LogPrint(reqStr)
+ return results, nil
+}
- return result
+// 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 000000000..52a380473
--- /dev/null
+++ b/enforcer_backslash_test.go
@@ -0,0 +1,177 @@
+// Copyright 2024 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..2c230db13
--- /dev/null
+++ b/enforcer_cached.go
@@ -0,0 +1,185 @@
+// Copyright 2018 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 {
+ irule := make([]interface{}, len(rules[0]))
+ for _, rule := range rules {
+ 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 000000000..2e2f780b5
--- /dev/null
+++ b/enforcer_cached_b_test.go
@@ -0,0 +1,221 @@
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..7d80b51a6
--- /dev/null
+++ b/enforcer_cached_gfunction_test.go
@@ -0,0 +1,201 @@
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..579281d69
--- /dev/null
+++ b/enforcer_cached_synced.go
@@ -0,0 +1,180 @@
+// Copyright 2018 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 {
+ irule := make([]interface{}, len(rules[0]))
+ for _, rule := range rules {
+ 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 000000000..06c84c482
--- /dev/null
+++ b/enforcer_cached_synced_test.go
@@ -0,0 +1,82 @@
+// Copyright 2018 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..34da2fa0f
--- /dev/null
+++ b/enforcer_cached_test.go
@@ -0,0 +1,73 @@
+// Copyright 2018 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..87bbffad8
--- /dev/null
+++ b/enforcer_context.go
@@ -0,0 +1,858 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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.Enforcer.adapter != nil {
+ if contextAdapter, ok := e.Enforcer.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 000000000..95c36c817
--- /dev/null
+++ b/enforcer_context_interface.go
@@ -0,0 +1,96 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..4f07d009e
--- /dev/null
+++ b/enforcer_context_test.go
@@ -0,0 +1,232 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..e3dce30ef
--- /dev/null
+++ b/enforcer_distributed.go
@@ -0,0 +1,239 @@
+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 000000000..94baf84ec
--- /dev/null
+++ b/enforcer_interface.go
@@ -0,0 +1,179 @@
+// Copyright 2019 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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{}
+var _ IEnforcer = &SyncedEnforcer{}
+var _ 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 000000000..6e5fc669a
--- /dev/null
+++ b/enforcer_json_test.go
@@ -0,0 +1,66 @@
+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_safe.go b/enforcer_safe.go
deleted file mode 100644
index d84023a34..000000000
--- a/enforcer_safe.go
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright 2017 The casbin Authors. All Rights Reserved.
-//
-// 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.
-
-package casbin
-
-import (
- "errors"
- "fmt"
-)
-
-// NewEnforcerSafe calls NewEnforcer in a safe way, returns error instead of causing panic.
-func NewEnforcerSafe(params ...interface{}) (e *Enforcer, err error) {
- defer func() {
- if r := recover(); r != nil {
- err = errors.New(fmt.Sprintf("%v", r))
- e = nil
- }
- }()
-
- e = NewEnforcer(params...)
- err = nil
- return
-}
-
-// LoadModelSafe calls LoadModel in a safe way, returns error instead of causing panic.
-func (e *Enforcer) LoadModelSafe() (err error) {
- defer func() {
- if r := recover(); r != nil {
- err = errors.New(fmt.Sprintf("%v", r))
- }
- }()
-
- e.LoadModel()
- err = nil
- return
-}
-
-// EnforceSafe calls Enforce in a safe way, returns error instead of causing panic.
-func (e *Enforcer) EnforceSafe(rvals ...interface{}) (result bool, err error) {
- defer func() {
- if r := recover(); r != nil {
- err = errors.New(fmt.Sprintf("%v", r))
- result = false
- }
- }()
-
- result = e.Enforce(rvals...)
- err = nil
- return
-}
-
-func (e *Enforcer) AddPolicySafe(params ...interface{}) (result bool, err error) {
- defer func() {
- if r := recover(); r != nil {
- err = errors.New(fmt.Sprintf("%v", r))
- result = false
- }
- }()
-
- result = e.AddNamedPolicy("p", params...)
- err = nil
- return
-}
-
-func (e *Enforcer) RemovePolicySafe(params ...interface{}) (result bool, err error) {
- defer func() {
- if r := recover(); r != nil {
- err = errors.New(fmt.Sprintf("%v", r))
- result = false
- }
- }()
-
- result = e.RemoveNamedPolicy("p", params...)
- err = nil
- return
-}
-
-func (e *Enforcer) RemoveFilteredPolicySafe(fieldIndex int, fieldValues ...string) (result bool, err error) {
- defer func() {
- if r := recover(); r != nil {
- err = errors.New(fmt.Sprintf("%v", r))
- result = false
- }
- }()
-
- result = e.RemoveFilteredNamedPolicy("p", fieldIndex, fieldValues...)
- err = nil
- return
-}
\ No newline at end of file
diff --git a/enforcer_synced.go b/enforcer_synced.go
index 473a31954..89bbe5dae 100644
--- a/enforcer_synced.go
+++ b/enforcer_synced.go
@@ -15,55 +15,124 @@
package casbin
import (
- "log"
"sync"
+ "sync/atomic"
"time"
- "github.com/casbin/casbin/persist"
+ "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
- autoLoad bool
+ m sync.RWMutex
+ stopAutoLoad chan struct{}
+ autoLoadRunning int32
}
-// NewEnforcer creates a synchronized enforcer via file or DB.
-func NewSyncedEnforcer(params ...interface{}) *SyncedEnforcer {
+// NewSyncedEnforcer creates a synchronized enforcer via file or DB.
+func NewSyncedEnforcer(params ...interface{}) (*SyncedEnforcer, error) {
e := &SyncedEnforcer{}
- e.Enforcer = NewEnforcer(params...)
- e.autoLoad = false
- return e
+ 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) {
- e.autoLoad = true
+ // 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
- log.Print("Start automatically load policy")
for {
- if !e.autoLoad {
- log.Print("Stop automatically load policy")
- break
+ 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
}
-
- e.LoadPolicy()
- // Uncomment this line to see when the policy is loaded.
- // log.Print("Load policy for time: ", n)
- n ++
- time.Sleep(d)
}
- } ()
+ }()
}
+// StopAutoLoadPolicy causes the go routine to exit.
func (e *SyncedEnforcer) StopAutoLoadPolicy() {
- e.autoLoad = false
+ if e.IsAutoLoadingRunning() {
+ e.stopAutoLoad <- struct{}{}
+ }
}
// SetWatcher sets the current watcher.
-func (e *SyncedEnforcer) SetWatcher(watcher persist.Watcher) {
- e.watcher = watcher
- watcher.SetUpdateCallback(func (string) {e.LoadPolicy()})
+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.
@@ -75,144 +144,543 @@ func (e *SyncedEnforcer) 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.LoadPolicy()
+ 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.RLock()
- defer e.m.RUnlock()
+ e.m.Lock()
+ defer e.m.Unlock()
return e.Enforcer.SavePolicy()
}
// BuildRoleLinks manually rebuild the role inheritance relations.
-func (e *SyncedEnforcer) BuildRoleLinks() {
- e.m.RLock()
- defer e.m.RUnlock()
- e.Enforcer.BuildRoleLinks()
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 000000000..100f01558
--- /dev/null
+++ b/enforcer_synced_test.go
@@ -0,0 +1,533 @@
+// Copyright 2018 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 49df7f29b..bc8f8b4be 100644
--- a/enforcer_test.go
+++ b/enforcer_test.go
@@ -15,14 +15,18 @@
package casbin
import (
+ "strings"
+ "sync"
"testing"
- "time"
- "github.com/casbin/casbin/persist/file-adapter"
+ "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 := NewModel()
+ 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))")
@@ -30,7 +34,7 @@ func TestKeyMatchModelInMemory(t *testing.T) {
a := fileadapter.NewAdapter("examples/keymatch_policy.csv")
- e := NewEnforcer(m, a)
+ e, _ := NewEnforcer(m, a)
testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true)
testEnforce(t, e, "alice", "/alice_data/resource1", "POST", true)
@@ -54,8 +58,8 @@ func TestKeyMatchModelInMemory(t *testing.T) {
testEnforce(t, e, "cathy", "/cathy_data", "POST", true)
testEnforce(t, e, "cathy", "/cathy_data", "DELETE", false)
- e = NewEnforcer(m)
- a.LoadPolicy(e.GetModel())
+ 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)
@@ -80,8 +84,13 @@ func TestKeyMatchModelInMemory(t *testing.T) {
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 := NewModel()
+ 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))")
@@ -89,41 +98,41 @@ func TestKeyMatchModelInMemoryDeny(t *testing.T) {
a := fileadapter.NewAdapter("examples/keymatch_policy.csv")
- e := NewEnforcer(m, a)
+ e, _ := NewEnforcer(m, a)
testEnforce(t, e, "alice", "/alice_data/resource2", "POST", true)
}
func TestRBACModelInMemoryIndeterminate(t *testing.T) {
- m := NewModel()
+ 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, _ := NewEnforcer(m)
- e.AddPermissionForUser("alice", "data1", "invalid")
+ _, _ = e.AddPermissionForUser("alice", "data1", "invalid")
testEnforce(t, e, "alice", "data1", "read", false)
}
func TestRBACModelInMemory(t *testing.T) {
- m := NewModel()
+ 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, _ := 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")
+ _, _ = 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)
@@ -153,18 +162,18 @@ e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
- m := NewModel(text)
+ m, _ := model.NewModelFromString(text)
// The above is the same as:
// m := NewModel()
// m.LoadModelFromText(text)
- e := NewEnforcer(m)
+ 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")
+ _, _ = 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)
@@ -177,17 +186,17 @@ m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
}
func TestNotUsedRBACModelInMemory(t *testing.T) {
- m := NewModel()
+ 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, _ := NewEnforcer(m)
- e.AddPermissionForUser("alice", "data1", "read")
- e.AddPermissionForUser("bob", "data2", "write")
+ _, _ = e.AddPermissionForUser("alice", "data1", "read")
+ _, _ = e.AddPermissionForUser("bob", "data2", "write")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -199,27 +208,52 @@ func TestNotUsedRBACModelInMemory(t *testing.T) {
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, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
- e.LoadPolicy()
+ _ = 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, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
- e.SavePolicy()
+ _ = e.SavePolicy()
}
func TestClearPolicy(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+ 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, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e.EnableEnforce(false)
testEnforce(t, e, "alice", "data1", "read", true)
@@ -243,9 +277,9 @@ func TestEnableEnforce(t *testing.T) {
}
func TestEnableLog(t *testing.T) {
- e := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv", true)
- // The log is enabled by default, so the above is the same with:
- // e := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
+ // 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)
@@ -255,28 +289,17 @@ func TestEnableLog(t *testing.T) {
testEnforce(t, e, "bob", "data1", "write", false)
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
-
- // The log can also be enabled or disabled at run-time.
- e.EnableLog(false)
- 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, _ := 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")
+ _, _ = e.RemovePolicy("alice", "data1", "read")
// Reload the policy from the storage to see the effect.
- e.LoadPolicy()
+ _ = e.LoadPolicy()
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
testEnforce(t, e, "alice", "data2", "read", false)
@@ -289,12 +312,12 @@ func TestEnableAutoSave(t *testing.T) {
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")
+ _, _ = 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()
+ _ = 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)
@@ -307,7 +330,7 @@ func TestEnableAutoSave(t *testing.T) {
func TestInitWithAdapter(t *testing.T) {
adapter := fileadapter.NewAdapter("examples/basic_policy.csv")
- e := NewEnforcer("examples/basic_model.conf", adapter)
+ e, _ := NewEnforcer("examples/basic_model.conf", adapter)
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -319,34 +342,40 @@ func TestInitWithAdapter(t *testing.T) {
testEnforce(t, e, "bob", "data2", "write", true)
}
-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)
+func TestRoleLinks(t *testing.T) {
+ e, _ := NewEnforcer("examples/rbac_model.conf")
+ e.EnableAutoBuildRoleLinks(false)
+ _ = e.BuildRoleLinks()
+ _, _ = e.Enforce("user501", "data9", "read")
+}
- 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)
+func TestEnforceConcurrency(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("Enforce is not concurrent")
+ }
+ }()
- // Stop the reloading policy periodically.
- e.StopAutoLoadPolicy()
-}
+ e, _ := NewEnforcer("examples/rbac_model.conf")
+ _ = e.LoadModel()
-func TestRoleLinks(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf")
- e.EnableAutoBuildRoleLinks(false)
- e.BuildRoleLinks()
- e.Enforce("user501", "data9", "read")
+ 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")
+ 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)
@@ -356,38 +385,36 @@ func TestGetAndSetModel(t *testing.T) {
}
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")
+ 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()
+ _ = 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")
+ 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()
+ _ = e.LoadPolicy()
testEnforce(t, e, "alice", "data1", "read", true)
}
func TestInitEmpty(t *testing.T) {
+ e, _ := NewEnforcer()
- e := NewEnforcer()
-
- m := NewModel()
+ 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))")
@@ -397,7 +424,380 @@ func TestInitEmpty(t *testing.T) {
e.SetModel(m)
e.SetAdapter(a)
- e.LoadPolicy()
+ _ = 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 000000000..c0abfb7dd
--- /dev/null
+++ b/enforcer_transactional.go
@@ -0,0 +1,121 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index c6074a010..fa0e9f777 100644
--- a/error_test.go
+++ b/error_test.go
@@ -15,108 +15,115 @@
package casbin
import (
- "fmt"
"testing"
- "github.com/casbin/casbin/persist/file-adapter"
+ fileadapter "github.com/casbin/casbin/v3/persist/file-adapter"
)
func TestPathError(t *testing.T) {
- _, err := NewEnforcerSafe("hope_this_path_wont_exist", "")
+ _, err := NewEnforcer("hope_this_path_wont_exist", "")
if err == nil {
t.Errorf("Should be error here.")
} else {
- fmt.Print("Test on error: ")
- fmt.Print(err.Error())
+ t.Log("Test on error: ")
+ t.Log(err.Error())
}
}
func TestEnforcerParamError(t *testing.T) {
- _, err := NewEnforcerSafe(1, 2, 3)
+ _, err := NewEnforcer(1, 2, 3)
if err == nil {
t.Errorf("Should not be error here.")
} else {
- fmt.Print("Test on error: ")
- fmt.Print(err.Error())
+ t.Log("Test on error: ")
+ t.Log(err.Error())
}
- _, err2 := NewEnforcerSafe(1, "2")
+ _, err2 := NewEnforcer(1, "2")
if err2 == nil {
t.Errorf("Should not be error here.")
} else {
- fmt.Print("Test on error: ")
- fmt.Print(err2.Error())
+ t.Log("Test on error: ")
+ t.Log(err2.Error())
}
}
func TestModelError(t *testing.T) {
- _, err := NewEnforcerSafe("examples/error/error_model.conf", "examples/error/error_policy.csv")
+ _, err := NewEnforcer("examples/error/error_model.conf", "examples/error/error_policy.csv")
if err == nil {
t.Errorf("Should be error here.")
} else {
- fmt.Print("Test on error: ")
- fmt.Print(err.Error())
+ t.Log("Test on error: ")
+ t.Log(err.Error())
}
}
-func TestPolicyError(t *testing.T) {
- _, err := NewEnforcerSafe("examples/basic_model.conf", "examples/error/error_policy.csv")
+// 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 {
- fmt.Print("Test on error: ")
- fmt.Print(err.Error())
+ 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.EnforceSafe("wrong", "wrong")
+ e, _ = NewEnforcer("examples/abac_rule_model.conf")
+ _, err = e.Enforce("wang", "wang", "wang")
if err == nil {
t.Errorf("Should be error here.")
} else {
- fmt.Print("Test on error: ")
- fmt.Print(err.Error())
+ t.Log("Test on error: ")
+ t.Log(err.Error())
}
}
func TestNoError(t *testing.T) {
- e := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
+ e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
- err := e.LoadModelSafe()
+ err := e.LoadModel()
if err != nil {
t.Errorf("Should be no error here.")
- fmt.Print("Unexpected error: ")
- fmt.Print(err.Error())
+ t.Log("Unexpected error: ")
+ t.Log(err.Error())
}
err = e.LoadPolicy()
if err != nil {
t.Errorf("Should be no error here.")
- fmt.Print("Unexpected error: ")
- fmt.Print(err.Error())
+ t.Log("Unexpected error: ")
+ t.Log(err.Error())
}
err = e.SavePolicy()
if err != nil {
t.Errorf("Should be no error here.")
- fmt.Print("Unexpected error: ")
- fmt.Print(err.Error())
+ t.Log("Unexpected error: ")
+ t.Log(err.Error())
}
}
func TestModelNoError(t *testing.T) {
- e := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
+ e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
e.modelPath = "hope_this_path_wont_exist"
- err := e.LoadModelSafe()
+ err := e.LoadModel()
if err == nil {
t.Errorf("Should be error here.")
} else {
- fmt.Print("Test on error: ")
- fmt.Print(err.Error())
+ t.Log("Test on error: ")
+ t.Log(err.Error())
}
}
@@ -124,32 +131,155 @@ func TestMockAdapterErrors(t *testing.T) {
adapter := fileadapter.NewAdapterMock("examples/rbac_with_domains_policy.csv")
adapter.SetMockErr("mock error")
- e, _ := NewEnforcerSafe("examples/rbac_with_domains_model.conf", adapter)
+ e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", adapter)
- _, err := e.AddPolicySafe("admin", "domain3", "data1", "read")
+ 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 {
- fmt.Print("Test on error: ")
- fmt.Print(err.Error())
+ t.Log("Test on error: ")
+ t.Log(err.Error())
}
- _, err2 := e.RemoveFilteredPolicySafe(1, "domain1", "data1")
+ 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 {
- fmt.Print("Test on error: ")
- fmt.Print(err2.Error())
+ t.Log("Test on error: ")
+ t.Log(err2.Error())
}
- _, err3 := e.RemovePolicySafe("admin", "domain2", "data2", "read")
+ 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 {
- fmt.Print("Test on error: ")
- fmt.Print(err3.Error())
+ 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 000000000..e2a0b8744
--- /dev/null
+++ b/errors/constraint_errors.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..2f358b372
--- /dev/null
+++ b/errors/rbac_errors.go
@@ -0,0 +1,30 @@
+// Copyright 2018 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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_not_using_policy_model.conf b/examples/abac_not_using_policy_model.conf
new file mode 100644
index 000000000..64c708a88
--- /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 000000000..bea996267
--- /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 000000000..591dd3a62
--- /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 000000000..e3dbc8331
--- /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_model_without_spaces.conf b/examples/basic_model_without_spaces.conf
new file mode 100644
index 000000000..5452f9542
--- /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/biba_model.conf b/examples/biba_model.conf
new file mode 100644
index 000000000..bfc03deb1
--- /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 000000000..3ee9fb2f7
--- /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 000000000..a4200ebf7
--- /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/eval_operator_model.conf b/examples/eval_operator_model.conf
new file mode 100644
index 000000000..9ffb5ba4c
--- /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 000000000..85665a87c
--- /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 000000000..b16cad499
--- /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 000000000..86c03b079
--- /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/keyget2_model.conf b/examples/keyget2_model.conf
new file mode 100644
index 000000000..5b569a685
--- /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 000000000..cc92832b5
--- /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/keymatch_with_rbac_in_domain.conf b/examples/keymatch_with_rbac_in_domain.conf
new file mode 100644
index 000000000..396fb451a
--- /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 000000000..300579f19
--- /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 000000000..be509a2e7
--- /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 000000000..b619097a2
--- /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 000000000..66498ab3f
--- /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 000000000..55aa18521
--- /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 000000000..7dad5c849
--- /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 000000000..ed2f4eef5
--- /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 000000000..91c3b4ba7
--- /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 000000000..23023bde9
--- /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 000000000..23a76f136
--- /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 000000000..19c405223
--- /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/performance/rbac_with_pattern_large_scale_policy.csv b/examples/performance/rbac_with_pattern_large_scale_policy.csv
new file mode 100644
index 000000000..5e865a7b8
--- /dev/null
+++ b/examples/performance/rbac_with_pattern_large_scale_policy.csv
@@ -0,0 +1,3768 @@
+# 132 policies / 3000 grouping policies / 300 subjuects / 6 roles
+# Policy - staff001
+p, staff001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1001
+p, staff001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1002
+p, staff001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1003
+p, staff001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1004
+p, staff001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1005
+p, staff001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2001
+p, staff001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2002
+p, staff001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2003
+p, staff001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2004
+p, staff001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2005
+
+p, staff001, /orgs/{orgID}/sites, App001.Module002.Action1006
+p, staff001, /orgs/{orgID}/sites, App001.Module002.Action1007
+p, staff001, /orgs/{orgID}/sites, App001.Module002.Action1008
+p, staff001, /orgs/{orgID}/sites, App001.Module002.Action1009
+p, staff001, /orgs/{orgID}/sites, App001.Module002.Action1010
+p, staff001, /orgs/{orgID}/sites, App003.*.Action3001
+p, staff001, /orgs/{orgID}/sites, App003.*.Action3002
+p, staff001, /orgs/{orgID}/sites, App003.*.Action3003
+p, staff001, /orgs/{orgID}/sites, App003.*.Action3004
+p, staff001, /orgs/{orgID}/sites, App003.*.Action3005
+
+p, staff001, /orgs/{orgID}, App004.*
+p, staff001, /orgs, App005.*
+
+# Policy - staff002
+p, staff002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1001
+p, staff002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1002
+p, staff002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1003
+p, staff002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1004
+p, staff002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1005
+p, staff002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2001
+p, staff002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2002
+p, staff002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2003
+p, staff002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2004
+p, staff002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2005
+
+p, staff002, /orgs/{orgID}/sites, App001.Module002.Action1006
+p, staff002, /orgs/{orgID}/sites, App001.Module002.Action1007
+p, staff002, /orgs/{orgID}/sites, App001.Module002.Action1008
+p, staff002, /orgs/{orgID}/sites, App001.Module002.Action1009
+p, staff002, /orgs/{orgID}/sites, App001.Module002.Action1010
+p, staff002, /orgs/{orgID}/sites, App003.*.Action3001
+p, staff002, /orgs/{orgID}/sites, App003.*.Action3002
+p, staff002, /orgs/{orgID}/sites, App003.*.Action3003
+p, staff002, /orgs/{orgID}/sites, App003.*.Action3004
+p, staff002, /orgs/{orgID}/sites, App003.*.Action3005
+
+p, staff002, /orgs/{orgID}, App004.*
+p, staff002, /orgs, App005.*
+
+# Policy - manager001
+p, manager001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1001
+p, manager001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1002
+p, manager001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1003
+p, manager001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1004
+p, manager001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1005
+p, manager001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2001
+p, manager001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2002
+p, manager001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2003
+p, manager001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2004
+p, manager001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2005
+
+p, manager001, /orgs/{orgID}/sites, App001.Module002.Action1006
+p, manager001, /orgs/{orgID}/sites, App001.Module002.Action1007
+p, manager001, /orgs/{orgID}/sites, App001.Module002.Action1008
+p, manager001, /orgs/{orgID}/sites, App001.Module002.Action1009
+p, manager001, /orgs/{orgID}/sites, App001.Module002.Action1010
+p, manager001, /orgs/{orgID}/sites, App003.*.Action3001
+p, manager001, /orgs/{orgID}/sites, App003.*.Action3002
+p, manager001, /orgs/{orgID}/sites, App003.*.Action3003
+p, manager001, /orgs/{orgID}/sites, App003.*.Action3004
+p, manager001, /orgs/{orgID}/sites, App003.*.Action3005
+
+p, manager001, /orgs/{orgID}, App004.*
+p, manager001, /orgs, App005.*
+
+# Policy - manager002
+p, manager002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1001
+p, manager002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1002
+p, manager002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1003
+p, manager002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1004
+p, manager002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1005
+p, manager002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2001
+p, manager002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2002
+p, manager002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2003
+p, manager002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2004
+p, manager002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2005
+
+p, manager002, /orgs/{orgID}/sites, App001.Module002.Action1006
+p, manager002, /orgs/{orgID}/sites, App001.Module002.Action1007
+p, manager002, /orgs/{orgID}/sites, App001.Module002.Action1008
+p, manager002, /orgs/{orgID}/sites, App001.Module002.Action1009
+p, manager002, /orgs/{orgID}/sites, App001.Module002.Action1010
+p, manager002, /orgs/{orgID}/sites, App003.*.Action3001
+p, manager002, /orgs/{orgID}/sites, App003.*.Action3002
+p, manager002, /orgs/{orgID}/sites, App003.*.Action3003
+p, manager002, /orgs/{orgID}/sites, App003.*.Action3004
+p, manager002, /orgs/{orgID}/sites, App003.*.Action3005
+
+p, manager002, /orgs/{orgID}, App004.*
+p, manager002, /orgs, App005.*
+
+# Policy - customer001
+p, customer001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1001
+p, customer001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1002
+p, customer001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1003
+p, customer001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1004
+p, customer001, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1005
+p, customer001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2001
+p, customer001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2002
+p, customer001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2003
+p, customer001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2004
+p, customer001, /orgs/{orgID}/sites/{siteID}, App002.*.Action2005
+
+p, customer001, /orgs/{orgID}/sites, App001.Module002.Action1006
+p, customer001, /orgs/{orgID}/sites, App001.Module002.Action1007
+p, customer001, /orgs/{orgID}/sites, App001.Module002.Action1008
+p, customer001, /orgs/{orgID}/sites, App001.Module002.Action1009
+p, customer001, /orgs/{orgID}/sites, App001.Module002.Action1010
+p, customer001, /orgs/{orgID}/sites, App003.*.Action3001
+p, customer001, /orgs/{orgID}/sites, App003.*.Action3002
+p, customer001, /orgs/{orgID}/sites, App003.*.Action3003
+p, customer001, /orgs/{orgID}/sites, App003.*.Action3004
+p, customer001, /orgs/{orgID}/sites, App003.*.Action3005
+
+p, customer001, /orgs/{orgID}, App004.*
+p, customer001, /orgs, App005.*
+
+# Policy - customer002
+p, customer002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1001
+p, customer002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1002
+p, customer002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1003
+p, customer002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1004
+p, customer002, /orgs/{orgID}/sites/{siteID}, App001.Module001.Action1005
+p, customer002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2001
+p, customer002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2002
+p, customer002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2003
+p, customer002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2004
+p, customer002, /orgs/{orgID}/sites/{siteID}, App002.*.Action2005
+
+p, customer002, /orgs/{orgID}/sites, App001.Module002.Action1006
+p, customer002, /orgs/{orgID}/sites, App001.Module002.Action1007
+p, customer002, /orgs/{orgID}/sites, App001.Module002.Action1008
+p, customer002, /orgs/{orgID}/sites, App001.Module002.Action1009
+p, customer002, /orgs/{orgID}/sites, App001.Module002.Action1010
+p, customer002, /orgs/{orgID}/sites, App003.*.Action3001
+p, customer002, /orgs/{orgID}/sites, App003.*.Action3002
+p, customer002, /orgs/{orgID}/sites, App003.*.Action3003
+p, customer002, /orgs/{orgID}/sites, App003.*.Action3004
+p, customer002, /orgs/{orgID}/sites, App003.*.Action3005
+
+p, customer002, /orgs/{orgID}, App004.*
+p, customer002, /orgs, App005.*
+
+# Group - staff001, / org1
+g, staffUser1001, staff001, /orgs/1/sites/site001
+g, staffUser1001, staff001, /orgs/1/sites/site002
+g, staffUser1001, staff001, /orgs/1/sites/site003
+g, staffUser1001, staff001, /orgs/1/sites/site004
+g, staffUser1001, staff001, /orgs/1/sites/site005
+
+g, staffUser1001, staff001, /orgs/1/sites/site001
+g, staffUser1001, staff001, /orgs/1/sites/site002
+g, staffUser1001, staff001, /orgs/1/sites/site003
+g, staffUser1001, staff001, /orgs/1/sites/site004
+g, staffUser1001, staff001, /orgs/1/sites/site005
+
+g, staffUser1003, staff001, /orgs/1/sites/site001
+g, staffUser1003, staff001, /orgs/1/sites/site002
+g, staffUser1003, staff001, /orgs/1/sites/site003
+g, staffUser1003, staff001, /orgs/1/sites/site004
+g, staffUser1003, staff001, /orgs/1/sites/site005
+
+g, staffUser1004, staff001, /orgs/1/sites/site001
+g, staffUser1004, staff001, /orgs/1/sites/site002
+g, staffUser1004, staff001, /orgs/1/sites/site003
+g, staffUser1004, staff001, /orgs/1/sites/site004
+g, staffUser1004, staff001, /orgs/1/sites/site005
+
+g, staffUser1005, staff001, /orgs/1/sites/site001
+g, staffUser1005, staff001, /orgs/1/sites/site002
+g, staffUser1005, staff001, /orgs/1/sites/site003
+g, staffUser1005, staff001, /orgs/1/sites/site004
+g, staffUser1005, staff001, /orgs/1/sites/site005
+
+g, staffUser1006, staff001, /orgs/1/sites/site001
+g, staffUser1006, staff001, /orgs/1/sites/site002
+g, staffUser1006, staff001, /orgs/1/sites/site003
+g, staffUser1006, staff001, /orgs/1/sites/site004
+g, staffUser1006, staff001, /orgs/1/sites/site005
+
+g, staffUser1007, staff001, /orgs/1/sites/site001
+g, staffUser1007, staff001, /orgs/1/sites/site002
+g, staffUser1007, staff001, /orgs/1/sites/site003
+g, staffUser1007, staff001, /orgs/1/sites/site004
+g, staffUser1007, staff001, /orgs/1/sites/site005
+
+g, staffUser1008, staff001, /orgs/1/sites/site001
+g, staffUser1008, staff001, /orgs/1/sites/site002
+g, staffUser1008, staff001, /orgs/1/sites/site003
+g, staffUser1008, staff001, /orgs/1/sites/site004
+g, staffUser1008, staff001, /orgs/1/sites/site005
+
+g, staffUser1009, staff001, /orgs/1/sites/site001
+g, staffUser1009, staff001, /orgs/1/sites/site002
+g, staffUser1009, staff001, /orgs/1/sites/site003
+g, staffUser1009, staff001, /orgs/1/sites/site004
+g, staffUser1009, staff001, /orgs/1/sites/site005
+
+g, staffUser1010, staff001, /orgs/1/sites/site001
+g, staffUser1010, staff001, /orgs/1/sites/site002
+g, staffUser1010, staff001, /orgs/1/sites/site003
+g, staffUser1010, staff001, /orgs/1/sites/site004
+g, staffUser1010, staff001, /orgs/1/sites/site005
+
+g, staffUser1011, staff001, /orgs/1/sites/site001
+g, staffUser1011, staff001, /orgs/1/sites/site002
+g, staffUser1011, staff001, /orgs/1/sites/site003
+g, staffUser1011, staff001, /orgs/1/sites/site004
+g, staffUser1011, staff001, /orgs/1/sites/site005
+
+g, staffUser1012, staff001, /orgs/1/sites/site001
+g, staffUser1012, staff001, /orgs/1/sites/site002
+g, staffUser1012, staff001, /orgs/1/sites/site003
+g, staffUser1012, staff001, /orgs/1/sites/site004
+g, staffUser1012, staff001, /orgs/1/sites/site005
+
+g, staffUser1013, staff001, /orgs/1/sites/site001
+g, staffUser1013, staff001, /orgs/1/sites/site002
+g, staffUser1013, staff001, /orgs/1/sites/site003
+g, staffUser1013, staff001, /orgs/1/sites/site004
+g, staffUser1013, staff001, /orgs/1/sites/site005
+
+g, staffUser1014, staff001, /orgs/1/sites/site001
+g, staffUser1014, staff001, /orgs/1/sites/site002
+g, staffUser1014, staff001, /orgs/1/sites/site003
+g, staffUser1014, staff001, /orgs/1/sites/site004
+g, staffUser1014, staff001, /orgs/1/sites/site005
+
+g, staffUser1015, staff001, /orgs/1/sites/site001
+g, staffUser1015, staff001, /orgs/1/sites/site002
+g, staffUser1015, staff001, /orgs/1/sites/site003
+g, staffUser1015, staff001, /orgs/1/sites/site004
+g, staffUser1015, staff001, /orgs/1/sites/site005
+
+g, staffUser1016, staff001, /orgs/1/sites/site001
+g, staffUser1016, staff001, /orgs/1/sites/site002
+g, staffUser1016, staff001, /orgs/1/sites/site003
+g, staffUser1016, staff001, /orgs/1/sites/site004
+g, staffUser1016, staff001, /orgs/1/sites/site005
+
+g, staffUser1017, staff001, /orgs/1/sites/site001
+g, staffUser1017, staff001, /orgs/1/sites/site002
+g, staffUser1017, staff001, /orgs/1/sites/site003
+g, staffUser1017, staff001, /orgs/1/sites/site004
+g, staffUser1017, staff001, /orgs/1/sites/site005
+
+g, staffUser1018, staff001, /orgs/1/sites/site001
+g, staffUser1018, staff001, /orgs/1/sites/site002
+g, staffUser1018, staff001, /orgs/1/sites/site003
+g, staffUser1018, staff001, /orgs/1/sites/site004
+g, staffUser1018, staff001, /orgs/1/sites/site005
+
+g, staffUser1019, staff001, /orgs/1/sites/site001
+g, staffUser1019, staff001, /orgs/1/sites/site002
+g, staffUser1019, staff001, /orgs/1/sites/site003
+g, staffUser1019, staff001, /orgs/1/sites/site004
+g, staffUser1019, staff001, /orgs/1/sites/site005
+
+g, staffUser1020, staff001, /orgs/1/sites/site001
+g, staffUser1020, staff001, /orgs/1/sites/site002
+g, staffUser1020, staff001, /orgs/1/sites/site003
+g, staffUser1020, staff001, /orgs/1/sites/site004
+g, staffUser1020, staff001, /orgs/1/sites/site005
+
+g, staffUser1021, staff001, /orgs/1/sites/site001
+g, staffUser1021, staff001, /orgs/1/sites/site002
+g, staffUser1021, staff001, /orgs/1/sites/site003
+g, staffUser1021, staff001, /orgs/1/sites/site004
+g, staffUser1021, staff001, /orgs/1/sites/site005
+
+g, staffUser1022, staff001, /orgs/1/sites/site001
+g, staffUser1022, staff001, /orgs/1/sites/site002
+g, staffUser1022, staff001, /orgs/1/sites/site003
+g, staffUser1022, staff001, /orgs/1/sites/site004
+g, staffUser1022, staff001, /orgs/1/sites/site005
+
+g, staffUser1023, staff001, /orgs/1/sites/site001
+g, staffUser1023, staff001, /orgs/1/sites/site002
+g, staffUser1023, staff001, /orgs/1/sites/site003
+g, staffUser1023, staff001, /orgs/1/sites/site004
+g, staffUser1023, staff001, /orgs/1/sites/site005
+
+g, staffUser1024, staff001, /orgs/1/sites/site001
+g, staffUser1024, staff001, /orgs/1/sites/site002
+g, staffUser1024, staff001, /orgs/1/sites/site003
+g, staffUser1024, staff001, /orgs/1/sites/site004
+g, staffUser1024, staff001, /orgs/1/sites/site005
+
+g, staffUser1025, staff001, /orgs/1/sites/site001
+g, staffUser1025, staff001, /orgs/1/sites/site002
+g, staffUser1025, staff001, /orgs/1/sites/site003
+g, staffUser1025, staff001, /orgs/1/sites/site004
+g, staffUser1025, staff001, /orgs/1/sites/site005
+
+g, staffUser1026, staff001, /orgs/1/sites/site001
+g, staffUser1026, staff001, /orgs/1/sites/site002
+g, staffUser1026, staff001, /orgs/1/sites/site003
+g, staffUser1026, staff001, /orgs/1/sites/site004
+g, staffUser1026, staff001, /orgs/1/sites/site005
+
+g, staffUser1027, staff001, /orgs/1/sites/site001
+g, staffUser1027, staff001, /orgs/1/sites/site002
+g, staffUser1027, staff001, /orgs/1/sites/site003
+g, staffUser1027, staff001, /orgs/1/sites/site004
+g, staffUser1027, staff001, /orgs/1/sites/site005
+
+g, staffUser1028, staff001, /orgs/1/sites/site001
+g, staffUser1028, staff001, /orgs/1/sites/site002
+g, staffUser1028, staff001, /orgs/1/sites/site003
+g, staffUser1028, staff001, /orgs/1/sites/site004
+g, staffUser1028, staff001, /orgs/1/sites/site005
+
+g, staffUser1029, staff001, /orgs/1/sites/site001
+g, staffUser1029, staff001, /orgs/1/sites/site002
+g, staffUser1029, staff001, /orgs/1/sites/site003
+g, staffUser1029, staff001, /orgs/1/sites/site004
+g, staffUser1029, staff001, /orgs/1/sites/site005
+
+g, staffUser1030, staff001, /orgs/1/sites/site001
+g, staffUser1030, staff001, /orgs/1/sites/site002
+g, staffUser1030, staff001, /orgs/1/sites/site003
+g, staffUser1030, staff001, /orgs/1/sites/site004
+g, staffUser1030, staff001, /orgs/1/sites/site005
+
+g, staffUser1031, staff001, /orgs/1/sites/site001
+g, staffUser1031, staff001, /orgs/1/sites/site002
+g, staffUser1031, staff001, /orgs/1/sites/site003
+g, staffUser1031, staff001, /orgs/1/sites/site004
+g, staffUser1031, staff001, /orgs/1/sites/site005
+
+g, staffUser1032, staff001, /orgs/1/sites/site001
+g, staffUser1032, staff001, /orgs/1/sites/site002
+g, staffUser1032, staff001, /orgs/1/sites/site003
+g, staffUser1032, staff001, /orgs/1/sites/site004
+g, staffUser1032, staff001, /orgs/1/sites/site005
+
+g, staffUser1033, staff001, /orgs/1/sites/site001
+g, staffUser1033, staff001, /orgs/1/sites/site002
+g, staffUser1033, staff001, /orgs/1/sites/site003
+g, staffUser1033, staff001, /orgs/1/sites/site004
+g, staffUser1033, staff001, /orgs/1/sites/site005
+
+g, staffUser1034, staff001, /orgs/1/sites/site001
+g, staffUser1034, staff001, /orgs/1/sites/site002
+g, staffUser1034, staff001, /orgs/1/sites/site003
+g, staffUser1034, staff001, /orgs/1/sites/site004
+g, staffUser1034, staff001, /orgs/1/sites/site005
+
+g, staffUser1035, staff001, /orgs/1/sites/site001
+g, staffUser1035, staff001, /orgs/1/sites/site002
+g, staffUser1035, staff001, /orgs/1/sites/site003
+g, staffUser1035, staff001, /orgs/1/sites/site004
+g, staffUser1035, staff001, /orgs/1/sites/site005
+
+g, staffUser1036, staff001, /orgs/1/sites/site001
+g, staffUser1036, staff001, /orgs/1/sites/site002
+g, staffUser1036, staff001, /orgs/1/sites/site003
+g, staffUser1036, staff001, /orgs/1/sites/site004
+g, staffUser1036, staff001, /orgs/1/sites/site005
+
+g, staffUser1037, staff001, /orgs/1/sites/site001
+g, staffUser1037, staff001, /orgs/1/sites/site002
+g, staffUser1037, staff001, /orgs/1/sites/site003
+g, staffUser1037, staff001, /orgs/1/sites/site004
+g, staffUser1037, staff001, /orgs/1/sites/site005
+
+g, staffUser1038, staff001, /orgs/1/sites/site001
+g, staffUser1038, staff001, /orgs/1/sites/site002
+g, staffUser1038, staff001, /orgs/1/sites/site003
+g, staffUser1038, staff001, /orgs/1/sites/site004
+g, staffUser1038, staff001, /orgs/1/sites/site005
+
+g, staffUser1039, staff001, /orgs/1/sites/site001
+g, staffUser1039, staff001, /orgs/1/sites/site002
+g, staffUser1039, staff001, /orgs/1/sites/site003
+g, staffUser1039, staff001, /orgs/1/sites/site004
+g, staffUser1039, staff001, /orgs/1/sites/site005
+
+g, staffUser1040, staff001, /orgs/1/sites/site001
+g, staffUser1040, staff001, /orgs/1/sites/site002
+g, staffUser1040, staff001, /orgs/1/sites/site003
+g, staffUser1040, staff001, /orgs/1/sites/site004
+g, staffUser1040, staff001, /orgs/1/sites/site005
+
+g, staffUser1041, staff001, /orgs/1/sites/site001
+g, staffUser1041, staff001, /orgs/1/sites/site002
+g, staffUser1041, staff001, /orgs/1/sites/site003
+g, staffUser1041, staff001, /orgs/1/sites/site004
+g, staffUser1041, staff001, /orgs/1/sites/site005
+
+g, staffUser1042, staff001, /orgs/1/sites/site001
+g, staffUser1042, staff001, /orgs/1/sites/site002
+g, staffUser1042, staff001, /orgs/1/sites/site003
+g, staffUser1042, staff001, /orgs/1/sites/site004
+g, staffUser1042, staff001, /orgs/1/sites/site005
+
+g, staffUser1043, staff001, /orgs/1/sites/site001
+g, staffUser1043, staff001, /orgs/1/sites/site002
+g, staffUser1043, staff001, /orgs/1/sites/site003
+g, staffUser1043, staff001, /orgs/1/sites/site004
+g, staffUser1043, staff001, /orgs/1/sites/site005
+
+g, staffUser1044, staff001, /orgs/1/sites/site001
+g, staffUser1044, staff001, /orgs/1/sites/site002
+g, staffUser1044, staff001, /orgs/1/sites/site003
+g, staffUser1044, staff001, /orgs/1/sites/site004
+g, staffUser1044, staff001, /orgs/1/sites/site005
+
+g, staffUser1045, staff001, /orgs/1/sites/site001
+g, staffUser1045, staff001, /orgs/1/sites/site002
+g, staffUser1045, staff001, /orgs/1/sites/site003
+g, staffUser1045, staff001, /orgs/1/sites/site004
+g, staffUser1045, staff001, /orgs/1/sites/site005
+
+g, staffUser1046, staff001, /orgs/1/sites/site001
+g, staffUser1046, staff001, /orgs/1/sites/site002
+g, staffUser1046, staff001, /orgs/1/sites/site003
+g, staffUser1046, staff001, /orgs/1/sites/site004
+g, staffUser1046, staff001, /orgs/1/sites/site005
+
+g, staffUser1047, staff001, /orgs/1/sites/site001
+g, staffUser1047, staff001, /orgs/1/sites/site002
+g, staffUser1047, staff001, /orgs/1/sites/site003
+g, staffUser1047, staff001, /orgs/1/sites/site004
+g, staffUser1047, staff001, /orgs/1/sites/site005
+
+g, staffUser1048, staff001, /orgs/1/sites/site001
+g, staffUser1048, staff001, /orgs/1/sites/site002
+g, staffUser1048, staff001, /orgs/1/sites/site003
+g, staffUser1048, staff001, /orgs/1/sites/site004
+g, staffUser1048, staff001, /orgs/1/sites/site005
+
+g, staffUser1049, staff001, /orgs/1/sites/site001
+g, staffUser1049, staff001, /orgs/1/sites/site002
+g, staffUser1049, staff001, /orgs/1/sites/site003
+g, staffUser1049, staff001, /orgs/1/sites/site004
+g, staffUser1049, staff001, /orgs/1/sites/site005
+
+g, staffUser1050, staff001, /orgs/1/sites/site001
+g, staffUser1050, staff001, /orgs/1/sites/site002
+g, staffUser1050, staff001, /orgs/1/sites/site003
+g, staffUser1050, staff001, /orgs/1/sites/site004
+g, staffUser1050, staff001, /orgs/1/sites/site005
+
+# Group - staff001, / org1
+g, staffUser2001, staff001, /orgs/1/sites/site001
+g, staffUser2001, staff001, /orgs/1/sites/site002
+g, staffUser2001, staff001, /orgs/1/sites/site003
+g, staffUser2001, staff001, /orgs/1/sites/site004
+g, staffUser2001, staff001, /orgs/1/sites/site005
+
+g, staffUser2001, staff001, /orgs/1/sites/site001
+g, staffUser2001, staff001, /orgs/1/sites/site002
+g, staffUser2001, staff001, /orgs/1/sites/site003
+g, staffUser2001, staff001, /orgs/1/sites/site004
+g, staffUser2001, staff001, /orgs/1/sites/site005
+
+g, staffUser2003, staff001, /orgs/1/sites/site001
+g, staffUser2003, staff001, /orgs/1/sites/site002
+g, staffUser2003, staff001, /orgs/1/sites/site003
+g, staffUser2003, staff001, /orgs/1/sites/site004
+g, staffUser2003, staff001, /orgs/1/sites/site005
+
+g, staffUser2004, staff001, /orgs/1/sites/site001
+g, staffUser2004, staff001, /orgs/1/sites/site002
+g, staffUser2004, staff001, /orgs/1/sites/site003
+g, staffUser2004, staff001, /orgs/1/sites/site004
+g, staffUser2004, staff001, /orgs/1/sites/site005
+
+g, staffUser2005, staff001, /orgs/1/sites/site001
+g, staffUser2005, staff001, /orgs/1/sites/site002
+g, staffUser2005, staff001, /orgs/1/sites/site003
+g, staffUser2005, staff001, /orgs/1/sites/site004
+g, staffUser2005, staff001, /orgs/1/sites/site005
+
+g, staffUser2006, staff001, /orgs/1/sites/site001
+g, staffUser2006, staff001, /orgs/1/sites/site002
+g, staffUser2006, staff001, /orgs/1/sites/site003
+g, staffUser2006, staff001, /orgs/1/sites/site004
+g, staffUser2006, staff001, /orgs/1/sites/site005
+
+g, staffUser2007, staff001, /orgs/1/sites/site001
+g, staffUser2007, staff001, /orgs/1/sites/site002
+g, staffUser2007, staff001, /orgs/1/sites/site003
+g, staffUser2007, staff001, /orgs/1/sites/site004
+g, staffUser2007, staff001, /orgs/1/sites/site005
+
+g, staffUser2008, staff001, /orgs/1/sites/site001
+g, staffUser2008, staff001, /orgs/1/sites/site002
+g, staffUser2008, staff001, /orgs/1/sites/site003
+g, staffUser2008, staff001, /orgs/1/sites/site004
+g, staffUser2008, staff001, /orgs/1/sites/site005
+
+g, staffUser2009, staff001, /orgs/1/sites/site001
+g, staffUser2009, staff001, /orgs/1/sites/site002
+g, staffUser2009, staff001, /orgs/1/sites/site003
+g, staffUser2009, staff001, /orgs/1/sites/site004
+g, staffUser2009, staff001, /orgs/1/sites/site005
+
+g, staffUser2010, staff001, /orgs/1/sites/site001
+g, staffUser2010, staff001, /orgs/1/sites/site002
+g, staffUser2010, staff001, /orgs/1/sites/site003
+g, staffUser2010, staff001, /orgs/1/sites/site004
+g, staffUser2010, staff001, /orgs/1/sites/site005
+
+g, staffUser2011, staff001, /orgs/1/sites/site001
+g, staffUser2011, staff001, /orgs/1/sites/site002
+g, staffUser2011, staff001, /orgs/1/sites/site003
+g, staffUser2011, staff001, /orgs/1/sites/site004
+g, staffUser2011, staff001, /orgs/1/sites/site005
+
+g, staffUser2012, staff001, /orgs/1/sites/site001
+g, staffUser2012, staff001, /orgs/1/sites/site002
+g, staffUser2012, staff001, /orgs/1/sites/site003
+g, staffUser2012, staff001, /orgs/1/sites/site004
+g, staffUser2012, staff001, /orgs/1/sites/site005
+
+g, staffUser2013, staff001, /orgs/1/sites/site001
+g, staffUser2013, staff001, /orgs/1/sites/site002
+g, staffUser2013, staff001, /orgs/1/sites/site003
+g, staffUser2013, staff001, /orgs/1/sites/site004
+g, staffUser2013, staff001, /orgs/1/sites/site005
+
+g, staffUser2014, staff001, /orgs/1/sites/site001
+g, staffUser2014, staff001, /orgs/1/sites/site002
+g, staffUser2014, staff001, /orgs/1/sites/site003
+g, staffUser2014, staff001, /orgs/1/sites/site004
+g, staffUser2014, staff001, /orgs/1/sites/site005
+
+g, staffUser2015, staff001, /orgs/1/sites/site001
+g, staffUser2015, staff001, /orgs/1/sites/site002
+g, staffUser2015, staff001, /orgs/1/sites/site003
+g, staffUser2015, staff001, /orgs/1/sites/site004
+g, staffUser2015, staff001, /orgs/1/sites/site005
+
+g, staffUser2016, staff001, /orgs/1/sites/site001
+g, staffUser2016, staff001, /orgs/1/sites/site002
+g, staffUser2016, staff001, /orgs/1/sites/site003
+g, staffUser2016, staff001, /orgs/1/sites/site004
+g, staffUser2016, staff001, /orgs/1/sites/site005
+
+g, staffUser2017, staff001, /orgs/1/sites/site001
+g, staffUser2017, staff001, /orgs/1/sites/site002
+g, staffUser2017, staff001, /orgs/1/sites/site003
+g, staffUser2017, staff001, /orgs/1/sites/site004
+g, staffUser2017, staff001, /orgs/1/sites/site005
+
+g, staffUser2018, staff001, /orgs/1/sites/site001
+g, staffUser2018, staff001, /orgs/1/sites/site002
+g, staffUser2018, staff001, /orgs/1/sites/site003
+g, staffUser2018, staff001, /orgs/1/sites/site004
+g, staffUser2018, staff001, /orgs/1/sites/site005
+
+g, staffUser2019, staff001, /orgs/1/sites/site001
+g, staffUser2019, staff001, /orgs/1/sites/site002
+g, staffUser2019, staff001, /orgs/1/sites/site003
+g, staffUser2019, staff001, /orgs/1/sites/site004
+g, staffUser2019, staff001, /orgs/1/sites/site005
+
+g, staffUser2020, staff001, /orgs/1/sites/site001
+g, staffUser2020, staff001, /orgs/1/sites/site002
+g, staffUser2020, staff001, /orgs/1/sites/site003
+g, staffUser2020, staff001, /orgs/1/sites/site004
+g, staffUser2020, staff001, /orgs/1/sites/site005
+
+g, staffUser2021, staff001, /orgs/1/sites/site001
+g, staffUser2021, staff001, /orgs/1/sites/site002
+g, staffUser2021, staff001, /orgs/1/sites/site003
+g, staffUser2021, staff001, /orgs/1/sites/site004
+g, staffUser2021, staff001, /orgs/1/sites/site005
+
+g, staffUser2022, staff001, /orgs/1/sites/site001
+g, staffUser2022, staff001, /orgs/1/sites/site002
+g, staffUser2022, staff001, /orgs/1/sites/site003
+g, staffUser2022, staff001, /orgs/1/sites/site004
+g, staffUser2022, staff001, /orgs/1/sites/site005
+
+g, staffUser2023, staff001, /orgs/1/sites/site001
+g, staffUser2023, staff001, /orgs/1/sites/site002
+g, staffUser2023, staff001, /orgs/1/sites/site003
+g, staffUser2023, staff001, /orgs/1/sites/site004
+g, staffUser2023, staff001, /orgs/1/sites/site005
+
+g, staffUser2024, staff001, /orgs/1/sites/site001
+g, staffUser2024, staff001, /orgs/1/sites/site002
+g, staffUser2024, staff001, /orgs/1/sites/site003
+g, staffUser2024, staff001, /orgs/1/sites/site004
+g, staffUser2024, staff001, /orgs/1/sites/site005
+
+g, staffUser2025, staff001, /orgs/1/sites/site001
+g, staffUser2025, staff001, /orgs/1/sites/site002
+g, staffUser2025, staff001, /orgs/1/sites/site003
+g, staffUser2025, staff001, /orgs/1/sites/site004
+g, staffUser2025, staff001, /orgs/1/sites/site005
+
+g, staffUser2026, staff001, /orgs/1/sites/site001
+g, staffUser2026, staff001, /orgs/1/sites/site002
+g, staffUser2026, staff001, /orgs/1/sites/site003
+g, staffUser2026, staff001, /orgs/1/sites/site004
+g, staffUser2026, staff001, /orgs/1/sites/site005
+
+g, staffUser2027, staff001, /orgs/1/sites/site001
+g, staffUser2027, staff001, /orgs/1/sites/site002
+g, staffUser2027, staff001, /orgs/1/sites/site003
+g, staffUser2027, staff001, /orgs/1/sites/site004
+g, staffUser2027, staff001, /orgs/1/sites/site005
+
+g, staffUser2028, staff001, /orgs/1/sites/site001
+g, staffUser2028, staff001, /orgs/1/sites/site002
+g, staffUser2028, staff001, /orgs/1/sites/site003
+g, staffUser2028, staff001, /orgs/1/sites/site004
+g, staffUser2028, staff001, /orgs/1/sites/site005
+
+g, staffUser2029, staff001, /orgs/1/sites/site001
+g, staffUser2029, staff001, /orgs/1/sites/site002
+g, staffUser2029, staff001, /orgs/1/sites/site003
+g, staffUser2029, staff001, /orgs/1/sites/site004
+g, staffUser2029, staff001, /orgs/1/sites/site005
+
+g, staffUser2030, staff001, /orgs/1/sites/site001
+g, staffUser2030, staff001, /orgs/1/sites/site002
+g, staffUser2030, staff001, /orgs/1/sites/site003
+g, staffUser2030, staff001, /orgs/1/sites/site004
+g, staffUser2030, staff001, /orgs/1/sites/site005
+
+g, staffUser2031, staff001, /orgs/1/sites/site001
+g, staffUser2031, staff001, /orgs/1/sites/site002
+g, staffUser2031, staff001, /orgs/1/sites/site003
+g, staffUser2031, staff001, /orgs/1/sites/site004
+g, staffUser2031, staff001, /orgs/1/sites/site005
+
+g, staffUser2032, staff001, /orgs/1/sites/site001
+g, staffUser2032, staff001, /orgs/1/sites/site002
+g, staffUser2032, staff001, /orgs/1/sites/site003
+g, staffUser2032, staff001, /orgs/1/sites/site004
+g, staffUser2032, staff001, /orgs/1/sites/site005
+
+g, staffUser2033, staff001, /orgs/1/sites/site001
+g, staffUser2033, staff001, /orgs/1/sites/site002
+g, staffUser2033, staff001, /orgs/1/sites/site003
+g, staffUser2033, staff001, /orgs/1/sites/site004
+g, staffUser2033, staff001, /orgs/1/sites/site005
+
+g, staffUser2034, staff001, /orgs/1/sites/site001
+g, staffUser2034, staff001, /orgs/1/sites/site002
+g, staffUser2034, staff001, /orgs/1/sites/site003
+g, staffUser2034, staff001, /orgs/1/sites/site004
+g, staffUser2034, staff001, /orgs/1/sites/site005
+
+g, staffUser2035, staff001, /orgs/1/sites/site001
+g, staffUser2035, staff001, /orgs/1/sites/site002
+g, staffUser2035, staff001, /orgs/1/sites/site003
+g, staffUser2035, staff001, /orgs/1/sites/site004
+g, staffUser2035, staff001, /orgs/1/sites/site005
+
+g, staffUser2036, staff001, /orgs/1/sites/site001
+g, staffUser2036, staff001, /orgs/1/sites/site002
+g, staffUser2036, staff001, /orgs/1/sites/site003
+g, staffUser2036, staff001, /orgs/1/sites/site004
+g, staffUser2036, staff001, /orgs/1/sites/site005
+
+g, staffUser2037, staff001, /orgs/1/sites/site001
+g, staffUser2037, staff001, /orgs/1/sites/site002
+g, staffUser2037, staff001, /orgs/1/sites/site003
+g, staffUser2037, staff001, /orgs/1/sites/site004
+g, staffUser2037, staff001, /orgs/1/sites/site005
+
+g, staffUser2038, staff001, /orgs/1/sites/site001
+g, staffUser2038, staff001, /orgs/1/sites/site002
+g, staffUser2038, staff001, /orgs/1/sites/site003
+g, staffUser2038, staff001, /orgs/1/sites/site004
+g, staffUser2038, staff001, /orgs/1/sites/site005
+
+g, staffUser2039, staff001, /orgs/1/sites/site001
+g, staffUser2039, staff001, /orgs/1/sites/site002
+g, staffUser2039, staff001, /orgs/1/sites/site003
+g, staffUser2039, staff001, /orgs/1/sites/site004
+g, staffUser2039, staff001, /orgs/1/sites/site005
+
+g, staffUser2040, staff001, /orgs/1/sites/site001
+g, staffUser2040, staff001, /orgs/1/sites/site002
+g, staffUser2040, staff001, /orgs/1/sites/site003
+g, staffUser2040, staff001, /orgs/1/sites/site004
+g, staffUser2040, staff001, /orgs/1/sites/site005
+
+g, staffUser2041, staff001, /orgs/1/sites/site001
+g, staffUser2041, staff001, /orgs/1/sites/site002
+g, staffUser2041, staff001, /orgs/1/sites/site003
+g, staffUser2041, staff001, /orgs/1/sites/site004
+g, staffUser2041, staff001, /orgs/1/sites/site005
+
+g, staffUser2042, staff001, /orgs/1/sites/site001
+g, staffUser2042, staff001, /orgs/1/sites/site002
+g, staffUser2042, staff001, /orgs/1/sites/site003
+g, staffUser2042, staff001, /orgs/1/sites/site004
+g, staffUser2042, staff001, /orgs/1/sites/site005
+
+g, staffUser2043, staff001, /orgs/1/sites/site001
+g, staffUser2043, staff001, /orgs/1/sites/site002
+g, staffUser2043, staff001, /orgs/1/sites/site003
+g, staffUser2043, staff001, /orgs/1/sites/site004
+g, staffUser2043, staff001, /orgs/1/sites/site005
+
+g, staffUser2044, staff001, /orgs/1/sites/site001
+g, staffUser2044, staff001, /orgs/1/sites/site002
+g, staffUser2044, staff001, /orgs/1/sites/site003
+g, staffUser2044, staff001, /orgs/1/sites/site004
+g, staffUser2044, staff001, /orgs/1/sites/site005
+
+g, staffUser2045, staff001, /orgs/1/sites/site001
+g, staffUser2045, staff001, /orgs/1/sites/site002
+g, staffUser2045, staff001, /orgs/1/sites/site003
+g, staffUser2045, staff001, /orgs/1/sites/site004
+g, staffUser2045, staff001, /orgs/1/sites/site005
+
+g, staffUser2046, staff001, /orgs/1/sites/site001
+g, staffUser2046, staff001, /orgs/1/sites/site002
+g, staffUser2046, staff001, /orgs/1/sites/site003
+g, staffUser2046, staff001, /orgs/1/sites/site004
+g, staffUser2046, staff001, /orgs/1/sites/site005
+
+g, staffUser2047, staff001, /orgs/1/sites/site001
+g, staffUser2047, staff001, /orgs/1/sites/site002
+g, staffUser2047, staff001, /orgs/1/sites/site003
+g, staffUser2047, staff001, /orgs/1/sites/site004
+g, staffUser2047, staff001, /orgs/1/sites/site005
+
+g, staffUser2048, staff001, /orgs/1/sites/site001
+g, staffUser2048, staff001, /orgs/1/sites/site002
+g, staffUser2048, staff001, /orgs/1/sites/site003
+g, staffUser2048, staff001, /orgs/1/sites/site004
+g, staffUser2048, staff001, /orgs/1/sites/site005
+
+g, staffUser2049, staff001, /orgs/1/sites/site001
+g, staffUser2049, staff001, /orgs/1/sites/site002
+g, staffUser2049, staff001, /orgs/1/sites/site003
+g, staffUser2049, staff001, /orgs/1/sites/site004
+g, staffUser2049, staff001, /orgs/1/sites/site005
+
+g, staffUser2050, staff001, /orgs/1/sites/site001
+g, staffUser2050, staff001, /orgs/1/sites/site002
+g, staffUser2050, staff001, /orgs/1/sites/site003
+g, staffUser2050, staff001, /orgs/1/sites/site004
+g, staffUser2050, staff001, /orgs/1/sites/site005
+
+# Group - manager001, / org1
+g, managerUser1001, manager001, /orgs/1/sites/site001
+g, managerUser1001, manager001, /orgs/1/sites/site002
+g, managerUser1001, manager001, /orgs/1/sites/site003
+g, managerUser1001, manager001, /orgs/1/sites/site004
+g, managerUser1001, manager001, /orgs/1/sites/site005
+
+g, managerUser1001, manager001, /orgs/1/sites/site001
+g, managerUser1001, manager001, /orgs/1/sites/site002
+g, managerUser1001, manager001, /orgs/1/sites/site003
+g, managerUser1001, manager001, /orgs/1/sites/site004
+g, managerUser1001, manager001, /orgs/1/sites/site005
+
+g, managerUser1003, manager001, /orgs/1/sites/site001
+g, managerUser1003, manager001, /orgs/1/sites/site002
+g, managerUser1003, manager001, /orgs/1/sites/site003
+g, managerUser1003, manager001, /orgs/1/sites/site004
+g, managerUser1003, manager001, /orgs/1/sites/site005
+
+g, managerUser1004, manager001, /orgs/1/sites/site001
+g, managerUser1004, manager001, /orgs/1/sites/site002
+g, managerUser1004, manager001, /orgs/1/sites/site003
+g, managerUser1004, manager001, /orgs/1/sites/site004
+g, managerUser1004, manager001, /orgs/1/sites/site005
+
+g, managerUser1005, manager001, /orgs/1/sites/site001
+g, managerUser1005, manager001, /orgs/1/sites/site002
+g, managerUser1005, manager001, /orgs/1/sites/site003
+g, managerUser1005, manager001, /orgs/1/sites/site004
+g, managerUser1005, manager001, /orgs/1/sites/site005
+
+g, managerUser1006, manager001, /orgs/1/sites/site001
+g, managerUser1006, manager001, /orgs/1/sites/site002
+g, managerUser1006, manager001, /orgs/1/sites/site003
+g, managerUser1006, manager001, /orgs/1/sites/site004
+g, managerUser1006, manager001, /orgs/1/sites/site005
+
+g, managerUser1007, manager001, /orgs/1/sites/site001
+g, managerUser1007, manager001, /orgs/1/sites/site002
+g, managerUser1007, manager001, /orgs/1/sites/site003
+g, managerUser1007, manager001, /orgs/1/sites/site004
+g, managerUser1007, manager001, /orgs/1/sites/site005
+
+g, managerUser1008, manager001, /orgs/1/sites/site001
+g, managerUser1008, manager001, /orgs/1/sites/site002
+g, managerUser1008, manager001, /orgs/1/sites/site003
+g, managerUser1008, manager001, /orgs/1/sites/site004
+g, managerUser1008, manager001, /orgs/1/sites/site005
+
+g, managerUser1009, manager001, /orgs/1/sites/site001
+g, managerUser1009, manager001, /orgs/1/sites/site002
+g, managerUser1009, manager001, /orgs/1/sites/site003
+g, managerUser1009, manager001, /orgs/1/sites/site004
+g, managerUser1009, manager001, /orgs/1/sites/site005
+
+g, managerUser1010, manager001, /orgs/1/sites/site001
+g, managerUser1010, manager001, /orgs/1/sites/site002
+g, managerUser1010, manager001, /orgs/1/sites/site003
+g, managerUser1010, manager001, /orgs/1/sites/site004
+g, managerUser1010, manager001, /orgs/1/sites/site005
+
+g, managerUser1011, manager001, /orgs/1/sites/site001
+g, managerUser1011, manager001, /orgs/1/sites/site002
+g, managerUser1011, manager001, /orgs/1/sites/site003
+g, managerUser1011, manager001, /orgs/1/sites/site004
+g, managerUser1011, manager001, /orgs/1/sites/site005
+
+g, managerUser1012, manager001, /orgs/1/sites/site001
+g, managerUser1012, manager001, /orgs/1/sites/site002
+g, managerUser1012, manager001, /orgs/1/sites/site003
+g, managerUser1012, manager001, /orgs/1/sites/site004
+g, managerUser1012, manager001, /orgs/1/sites/site005
+
+g, managerUser1013, manager001, /orgs/1/sites/site001
+g, managerUser1013, manager001, /orgs/1/sites/site002
+g, managerUser1013, manager001, /orgs/1/sites/site003
+g, managerUser1013, manager001, /orgs/1/sites/site004
+g, managerUser1013, manager001, /orgs/1/sites/site005
+
+g, managerUser1014, manager001, /orgs/1/sites/site001
+g, managerUser1014, manager001, /orgs/1/sites/site002
+g, managerUser1014, manager001, /orgs/1/sites/site003
+g, managerUser1014, manager001, /orgs/1/sites/site004
+g, managerUser1014, manager001, /orgs/1/sites/site005
+
+g, managerUser1015, manager001, /orgs/1/sites/site001
+g, managerUser1015, manager001, /orgs/1/sites/site002
+g, managerUser1015, manager001, /orgs/1/sites/site003
+g, managerUser1015, manager001, /orgs/1/sites/site004
+g, managerUser1015, manager001, /orgs/1/sites/site005
+
+g, managerUser1016, manager001, /orgs/1/sites/site001
+g, managerUser1016, manager001, /orgs/1/sites/site002
+g, managerUser1016, manager001, /orgs/1/sites/site003
+g, managerUser1016, manager001, /orgs/1/sites/site004
+g, managerUser1016, manager001, /orgs/1/sites/site005
+
+g, managerUser1017, manager001, /orgs/1/sites/site001
+g, managerUser1017, manager001, /orgs/1/sites/site002
+g, managerUser1017, manager001, /orgs/1/sites/site003
+g, managerUser1017, manager001, /orgs/1/sites/site004
+g, managerUser1017, manager001, /orgs/1/sites/site005
+
+g, managerUser1018, manager001, /orgs/1/sites/site001
+g, managerUser1018, manager001, /orgs/1/sites/site002
+g, managerUser1018, manager001, /orgs/1/sites/site003
+g, managerUser1018, manager001, /orgs/1/sites/site004
+g, managerUser1018, manager001, /orgs/1/sites/site005
+
+g, managerUser1019, manager001, /orgs/1/sites/site001
+g, managerUser1019, manager001, /orgs/1/sites/site002
+g, managerUser1019, manager001, /orgs/1/sites/site003
+g, managerUser1019, manager001, /orgs/1/sites/site004
+g, managerUser1019, manager001, /orgs/1/sites/site005
+
+g, managerUser1020, manager001, /orgs/1/sites/site001
+g, managerUser1020, manager001, /orgs/1/sites/site002
+g, managerUser1020, manager001, /orgs/1/sites/site003
+g, managerUser1020, manager001, /orgs/1/sites/site004
+g, managerUser1020, manager001, /orgs/1/sites/site005
+
+g, managerUser1021, manager001, /orgs/1/sites/site001
+g, managerUser1021, manager001, /orgs/1/sites/site002
+g, managerUser1021, manager001, /orgs/1/sites/site003
+g, managerUser1021, manager001, /orgs/1/sites/site004
+g, managerUser1021, manager001, /orgs/1/sites/site005
+
+g, managerUser1022, manager001, /orgs/1/sites/site001
+g, managerUser1022, manager001, /orgs/1/sites/site002
+g, managerUser1022, manager001, /orgs/1/sites/site003
+g, managerUser1022, manager001, /orgs/1/sites/site004
+g, managerUser1022, manager001, /orgs/1/sites/site005
+
+g, managerUser1023, manager001, /orgs/1/sites/site001
+g, managerUser1023, manager001, /orgs/1/sites/site002
+g, managerUser1023, manager001, /orgs/1/sites/site003
+g, managerUser1023, manager001, /orgs/1/sites/site004
+g, managerUser1023, manager001, /orgs/1/sites/site005
+
+g, managerUser1024, manager001, /orgs/1/sites/site001
+g, managerUser1024, manager001, /orgs/1/sites/site002
+g, managerUser1024, manager001, /orgs/1/sites/site003
+g, managerUser1024, manager001, /orgs/1/sites/site004
+g, managerUser1024, manager001, /orgs/1/sites/site005
+
+g, managerUser1025, manager001, /orgs/1/sites/site001
+g, managerUser1025, manager001, /orgs/1/sites/site002
+g, managerUser1025, manager001, /orgs/1/sites/site003
+g, managerUser1025, manager001, /orgs/1/sites/site004
+g, managerUser1025, manager001, /orgs/1/sites/site005
+
+g, managerUser1026, manager001, /orgs/1/sites/site001
+g, managerUser1026, manager001, /orgs/1/sites/site002
+g, managerUser1026, manager001, /orgs/1/sites/site003
+g, managerUser1026, manager001, /orgs/1/sites/site004
+g, managerUser1026, manager001, /orgs/1/sites/site005
+
+g, managerUser1027, manager001, /orgs/1/sites/site001
+g, managerUser1027, manager001, /orgs/1/sites/site002
+g, managerUser1027, manager001, /orgs/1/sites/site003
+g, managerUser1027, manager001, /orgs/1/sites/site004
+g, managerUser1027, manager001, /orgs/1/sites/site005
+
+g, managerUser1028, manager001, /orgs/1/sites/site001
+g, managerUser1028, manager001, /orgs/1/sites/site002
+g, managerUser1028, manager001, /orgs/1/sites/site003
+g, managerUser1028, manager001, /orgs/1/sites/site004
+g, managerUser1028, manager001, /orgs/1/sites/site005
+
+g, managerUser1029, manager001, /orgs/1/sites/site001
+g, managerUser1029, manager001, /orgs/1/sites/site002
+g, managerUser1029, manager001, /orgs/1/sites/site003
+g, managerUser1029, manager001, /orgs/1/sites/site004
+g, managerUser1029, manager001, /orgs/1/sites/site005
+
+g, managerUser1030, manager001, /orgs/1/sites/site001
+g, managerUser1030, manager001, /orgs/1/sites/site002
+g, managerUser1030, manager001, /orgs/1/sites/site003
+g, managerUser1030, manager001, /orgs/1/sites/site004
+g, managerUser1030, manager001, /orgs/1/sites/site005
+
+g, managerUser1031, manager001, /orgs/1/sites/site001
+g, managerUser1031, manager001, /orgs/1/sites/site002
+g, managerUser1031, manager001, /orgs/1/sites/site003
+g, managerUser1031, manager001, /orgs/1/sites/site004
+g, managerUser1031, manager001, /orgs/1/sites/site005
+
+g, managerUser1032, manager001, /orgs/1/sites/site001
+g, managerUser1032, manager001, /orgs/1/sites/site002
+g, managerUser1032, manager001, /orgs/1/sites/site003
+g, managerUser1032, manager001, /orgs/1/sites/site004
+g, managerUser1032, manager001, /orgs/1/sites/site005
+
+g, managerUser1033, manager001, /orgs/1/sites/site001
+g, managerUser1033, manager001, /orgs/1/sites/site002
+g, managerUser1033, manager001, /orgs/1/sites/site003
+g, managerUser1033, manager001, /orgs/1/sites/site004
+g, managerUser1033, manager001, /orgs/1/sites/site005
+
+g, managerUser1034, manager001, /orgs/1/sites/site001
+g, managerUser1034, manager001, /orgs/1/sites/site002
+g, managerUser1034, manager001, /orgs/1/sites/site003
+g, managerUser1034, manager001, /orgs/1/sites/site004
+g, managerUser1034, manager001, /orgs/1/sites/site005
+
+g, managerUser1035, manager001, /orgs/1/sites/site001
+g, managerUser1035, manager001, /orgs/1/sites/site002
+g, managerUser1035, manager001, /orgs/1/sites/site003
+g, managerUser1035, manager001, /orgs/1/sites/site004
+g, managerUser1035, manager001, /orgs/1/sites/site005
+
+g, managerUser1036, manager001, /orgs/1/sites/site001
+g, managerUser1036, manager001, /orgs/1/sites/site002
+g, managerUser1036, manager001, /orgs/1/sites/site003
+g, managerUser1036, manager001, /orgs/1/sites/site004
+g, managerUser1036, manager001, /orgs/1/sites/site005
+
+g, managerUser1037, manager001, /orgs/1/sites/site001
+g, managerUser1037, manager001, /orgs/1/sites/site002
+g, managerUser1037, manager001, /orgs/1/sites/site003
+g, managerUser1037, manager001, /orgs/1/sites/site004
+g, managerUser1037, manager001, /orgs/1/sites/site005
+
+g, managerUser1038, manager001, /orgs/1/sites/site001
+g, managerUser1038, manager001, /orgs/1/sites/site002
+g, managerUser1038, manager001, /orgs/1/sites/site003
+g, managerUser1038, manager001, /orgs/1/sites/site004
+g, managerUser1038, manager001, /orgs/1/sites/site005
+
+g, managerUser1039, manager001, /orgs/1/sites/site001
+g, managerUser1039, manager001, /orgs/1/sites/site002
+g, managerUser1039, manager001, /orgs/1/sites/site003
+g, managerUser1039, manager001, /orgs/1/sites/site004
+g, managerUser1039, manager001, /orgs/1/sites/site005
+
+g, managerUser1040, manager001, /orgs/1/sites/site001
+g, managerUser1040, manager001, /orgs/1/sites/site002
+g, managerUser1040, manager001, /orgs/1/sites/site003
+g, managerUser1040, manager001, /orgs/1/sites/site004
+g, managerUser1040, manager001, /orgs/1/sites/site005
+
+g, managerUser1041, manager001, /orgs/1/sites/site001
+g, managerUser1041, manager001, /orgs/1/sites/site002
+g, managerUser1041, manager001, /orgs/1/sites/site003
+g, managerUser1041, manager001, /orgs/1/sites/site004
+g, managerUser1041, manager001, /orgs/1/sites/site005
+
+g, managerUser1042, manager001, /orgs/1/sites/site001
+g, managerUser1042, manager001, /orgs/1/sites/site002
+g, managerUser1042, manager001, /orgs/1/sites/site003
+g, managerUser1042, manager001, /orgs/1/sites/site004
+g, managerUser1042, manager001, /orgs/1/sites/site005
+
+g, managerUser1043, manager001, /orgs/1/sites/site001
+g, managerUser1043, manager001, /orgs/1/sites/site002
+g, managerUser1043, manager001, /orgs/1/sites/site003
+g, managerUser1043, manager001, /orgs/1/sites/site004
+g, managerUser1043, manager001, /orgs/1/sites/site005
+
+g, managerUser1044, manager001, /orgs/1/sites/site001
+g, managerUser1044, manager001, /orgs/1/sites/site002
+g, managerUser1044, manager001, /orgs/1/sites/site003
+g, managerUser1044, manager001, /orgs/1/sites/site004
+g, managerUser1044, manager001, /orgs/1/sites/site005
+
+g, managerUser1045, manager001, /orgs/1/sites/site001
+g, managerUser1045, manager001, /orgs/1/sites/site002
+g, managerUser1045, manager001, /orgs/1/sites/site003
+g, managerUser1045, manager001, /orgs/1/sites/site004
+g, managerUser1045, manager001, /orgs/1/sites/site005
+
+g, managerUser1046, manager001, /orgs/1/sites/site001
+g, managerUser1046, manager001, /orgs/1/sites/site002
+g, managerUser1046, manager001, /orgs/1/sites/site003
+g, managerUser1046, manager001, /orgs/1/sites/site004
+g, managerUser1046, manager001, /orgs/1/sites/site005
+
+g, managerUser1047, manager001, /orgs/1/sites/site001
+g, managerUser1047, manager001, /orgs/1/sites/site002
+g, managerUser1047, manager001, /orgs/1/sites/site003
+g, managerUser1047, manager001, /orgs/1/sites/site004
+g, managerUser1047, manager001, /orgs/1/sites/site005
+
+g, managerUser1048, manager001, /orgs/1/sites/site001
+g, managerUser1048, manager001, /orgs/1/sites/site002
+g, managerUser1048, manager001, /orgs/1/sites/site003
+g, managerUser1048, manager001, /orgs/1/sites/site004
+g, managerUser1048, manager001, /orgs/1/sites/site005
+
+g, managerUser1049, manager001, /orgs/1/sites/site001
+g, managerUser1049, manager001, /orgs/1/sites/site002
+g, managerUser1049, manager001, /orgs/1/sites/site003
+g, managerUser1049, manager001, /orgs/1/sites/site004
+g, managerUser1049, manager001, /orgs/1/sites/site005
+
+g, managerUser1050, manager001, /orgs/1/sites/site001
+g, managerUser1050, manager001, /orgs/1/sites/site002
+g, managerUser1050, manager001, /orgs/1/sites/site003
+g, managerUser1050, manager001, /orgs/1/sites/site004
+g, managerUser1050, manager001, /orgs/1/sites/site005
+
+# Group - manager001, / org1
+g, managerUser2001, manager001, /orgs/1/sites/site001
+g, managerUser2001, manager001, /orgs/1/sites/site002
+g, managerUser2001, manager001, /orgs/1/sites/site003
+g, managerUser2001, manager001, /orgs/1/sites/site004
+g, managerUser2001, manager001, /orgs/1/sites/site005
+
+g, managerUser2001, manager001, /orgs/1/sites/site001
+g, managerUser2001, manager001, /orgs/1/sites/site002
+g, managerUser2001, manager001, /orgs/1/sites/site003
+g, managerUser2001, manager001, /orgs/1/sites/site004
+g, managerUser2001, manager001, /orgs/1/sites/site005
+
+g, managerUser2003, manager001, /orgs/1/sites/site001
+g, managerUser2003, manager001, /orgs/1/sites/site002
+g, managerUser2003, manager001, /orgs/1/sites/site003
+g, managerUser2003, manager001, /orgs/1/sites/site004
+g, managerUser2003, manager001, /orgs/1/sites/site005
+
+g, managerUser2004, manager001, /orgs/1/sites/site001
+g, managerUser2004, manager001, /orgs/1/sites/site002
+g, managerUser2004, manager001, /orgs/1/sites/site003
+g, managerUser2004, manager001, /orgs/1/sites/site004
+g, managerUser2004, manager001, /orgs/1/sites/site005
+
+g, managerUser2005, manager001, /orgs/1/sites/site001
+g, managerUser2005, manager001, /orgs/1/sites/site002
+g, managerUser2005, manager001, /orgs/1/sites/site003
+g, managerUser2005, manager001, /orgs/1/sites/site004
+g, managerUser2005, manager001, /orgs/1/sites/site005
+
+g, managerUser2006, manager001, /orgs/1/sites/site001
+g, managerUser2006, manager001, /orgs/1/sites/site002
+g, managerUser2006, manager001, /orgs/1/sites/site003
+g, managerUser2006, manager001, /orgs/1/sites/site004
+g, managerUser2006, manager001, /orgs/1/sites/site005
+
+g, managerUser2007, manager001, /orgs/1/sites/site001
+g, managerUser2007, manager001, /orgs/1/sites/site002
+g, managerUser2007, manager001, /orgs/1/sites/site003
+g, managerUser2007, manager001, /orgs/1/sites/site004
+g, managerUser2007, manager001, /orgs/1/sites/site005
+
+g, managerUser2008, manager001, /orgs/1/sites/site001
+g, managerUser2008, manager001, /orgs/1/sites/site002
+g, managerUser2008, manager001, /orgs/1/sites/site003
+g, managerUser2008, manager001, /orgs/1/sites/site004
+g, managerUser2008, manager001, /orgs/1/sites/site005
+
+g, managerUser2009, manager001, /orgs/1/sites/site001
+g, managerUser2009, manager001, /orgs/1/sites/site002
+g, managerUser2009, manager001, /orgs/1/sites/site003
+g, managerUser2009, manager001, /orgs/1/sites/site004
+g, managerUser2009, manager001, /orgs/1/sites/site005
+
+g, managerUser2010, manager001, /orgs/1/sites/site001
+g, managerUser2010, manager001, /orgs/1/sites/site002
+g, managerUser2010, manager001, /orgs/1/sites/site003
+g, managerUser2010, manager001, /orgs/1/sites/site004
+g, managerUser2010, manager001, /orgs/1/sites/site005
+
+g, managerUser2011, manager001, /orgs/1/sites/site001
+g, managerUser2011, manager001, /orgs/1/sites/site002
+g, managerUser2011, manager001, /orgs/1/sites/site003
+g, managerUser2011, manager001, /orgs/1/sites/site004
+g, managerUser2011, manager001, /orgs/1/sites/site005
+
+g, managerUser2012, manager001, /orgs/1/sites/site001
+g, managerUser2012, manager001, /orgs/1/sites/site002
+g, managerUser2012, manager001, /orgs/1/sites/site003
+g, managerUser2012, manager001, /orgs/1/sites/site004
+g, managerUser2012, manager001, /orgs/1/sites/site005
+
+g, managerUser2013, manager001, /orgs/1/sites/site001
+g, managerUser2013, manager001, /orgs/1/sites/site002
+g, managerUser2013, manager001, /orgs/1/sites/site003
+g, managerUser2013, manager001, /orgs/1/sites/site004
+g, managerUser2013, manager001, /orgs/1/sites/site005
+
+g, managerUser2014, manager001, /orgs/1/sites/site001
+g, managerUser2014, manager001, /orgs/1/sites/site002
+g, managerUser2014, manager001, /orgs/1/sites/site003
+g, managerUser2014, manager001, /orgs/1/sites/site004
+g, managerUser2014, manager001, /orgs/1/sites/site005
+
+g, managerUser2015, manager001, /orgs/1/sites/site001
+g, managerUser2015, manager001, /orgs/1/sites/site002
+g, managerUser2015, manager001, /orgs/1/sites/site003
+g, managerUser2015, manager001, /orgs/1/sites/site004
+g, managerUser2015, manager001, /orgs/1/sites/site005
+
+g, managerUser2016, manager001, /orgs/1/sites/site001
+g, managerUser2016, manager001, /orgs/1/sites/site002
+g, managerUser2016, manager001, /orgs/1/sites/site003
+g, managerUser2016, manager001, /orgs/1/sites/site004
+g, managerUser2016, manager001, /orgs/1/sites/site005
+
+g, managerUser2017, manager001, /orgs/1/sites/site001
+g, managerUser2017, manager001, /orgs/1/sites/site002
+g, managerUser2017, manager001, /orgs/1/sites/site003
+g, managerUser2017, manager001, /orgs/1/sites/site004
+g, managerUser2017, manager001, /orgs/1/sites/site005
+
+g, managerUser2018, manager001, /orgs/1/sites/site001
+g, managerUser2018, manager001, /orgs/1/sites/site002
+g, managerUser2018, manager001, /orgs/1/sites/site003
+g, managerUser2018, manager001, /orgs/1/sites/site004
+g, managerUser2018, manager001, /orgs/1/sites/site005
+
+g, managerUser2019, manager001, /orgs/1/sites/site001
+g, managerUser2019, manager001, /orgs/1/sites/site002
+g, managerUser2019, manager001, /orgs/1/sites/site003
+g, managerUser2019, manager001, /orgs/1/sites/site004
+g, managerUser2019, manager001, /orgs/1/sites/site005
+
+g, managerUser2020, manager001, /orgs/1/sites/site001
+g, managerUser2020, manager001, /orgs/1/sites/site002
+g, managerUser2020, manager001, /orgs/1/sites/site003
+g, managerUser2020, manager001, /orgs/1/sites/site004
+g, managerUser2020, manager001, /orgs/1/sites/site005
+
+g, managerUser2021, manager001, /orgs/1/sites/site001
+g, managerUser2021, manager001, /orgs/1/sites/site002
+g, managerUser2021, manager001, /orgs/1/sites/site003
+g, managerUser2021, manager001, /orgs/1/sites/site004
+g, managerUser2021, manager001, /orgs/1/sites/site005
+
+g, managerUser2022, manager001, /orgs/1/sites/site001
+g, managerUser2022, manager001, /orgs/1/sites/site002
+g, managerUser2022, manager001, /orgs/1/sites/site003
+g, managerUser2022, manager001, /orgs/1/sites/site004
+g, managerUser2022, manager001, /orgs/1/sites/site005
+
+g, managerUser2023, manager001, /orgs/1/sites/site001
+g, managerUser2023, manager001, /orgs/1/sites/site002
+g, managerUser2023, manager001, /orgs/1/sites/site003
+g, managerUser2023, manager001, /orgs/1/sites/site004
+g, managerUser2023, manager001, /orgs/1/sites/site005
+
+g, managerUser2024, manager001, /orgs/1/sites/site001
+g, managerUser2024, manager001, /orgs/1/sites/site002
+g, managerUser2024, manager001, /orgs/1/sites/site003
+g, managerUser2024, manager001, /orgs/1/sites/site004
+g, managerUser2024, manager001, /orgs/1/sites/site005
+
+g, managerUser2025, manager001, /orgs/1/sites/site001
+g, managerUser2025, manager001, /orgs/1/sites/site002
+g, managerUser2025, manager001, /orgs/1/sites/site003
+g, managerUser2025, manager001, /orgs/1/sites/site004
+g, managerUser2025, manager001, /orgs/1/sites/site005
+
+g, managerUser2026, manager001, /orgs/1/sites/site001
+g, managerUser2026, manager001, /orgs/1/sites/site002
+g, managerUser2026, manager001, /orgs/1/sites/site003
+g, managerUser2026, manager001, /orgs/1/sites/site004
+g, managerUser2026, manager001, /orgs/1/sites/site005
+
+g, managerUser2027, manager001, /orgs/1/sites/site001
+g, managerUser2027, manager001, /orgs/1/sites/site002
+g, managerUser2027, manager001, /orgs/1/sites/site003
+g, managerUser2027, manager001, /orgs/1/sites/site004
+g, managerUser2027, manager001, /orgs/1/sites/site005
+
+g, managerUser2028, manager001, /orgs/1/sites/site001
+g, managerUser2028, manager001, /orgs/1/sites/site002
+g, managerUser2028, manager001, /orgs/1/sites/site003
+g, managerUser2028, manager001, /orgs/1/sites/site004
+g, managerUser2028, manager001, /orgs/1/sites/site005
+
+g, managerUser2029, manager001, /orgs/1/sites/site001
+g, managerUser2029, manager001, /orgs/1/sites/site002
+g, managerUser2029, manager001, /orgs/1/sites/site003
+g, managerUser2029, manager001, /orgs/1/sites/site004
+g, managerUser2029, manager001, /orgs/1/sites/site005
+
+g, managerUser2030, manager001, /orgs/1/sites/site001
+g, managerUser2030, manager001, /orgs/1/sites/site002
+g, managerUser2030, manager001, /orgs/1/sites/site003
+g, managerUser2030, manager001, /orgs/1/sites/site004
+g, managerUser2030, manager001, /orgs/1/sites/site005
+
+g, managerUser2031, manager001, /orgs/1/sites/site001
+g, managerUser2031, manager001, /orgs/1/sites/site002
+g, managerUser2031, manager001, /orgs/1/sites/site003
+g, managerUser2031, manager001, /orgs/1/sites/site004
+g, managerUser2031, manager001, /orgs/1/sites/site005
+
+g, managerUser2032, manager001, /orgs/1/sites/site001
+g, managerUser2032, manager001, /orgs/1/sites/site002
+g, managerUser2032, manager001, /orgs/1/sites/site003
+g, managerUser2032, manager001, /orgs/1/sites/site004
+g, managerUser2032, manager001, /orgs/1/sites/site005
+
+g, managerUser2033, manager001, /orgs/1/sites/site001
+g, managerUser2033, manager001, /orgs/1/sites/site002
+g, managerUser2033, manager001, /orgs/1/sites/site003
+g, managerUser2033, manager001, /orgs/1/sites/site004
+g, managerUser2033, manager001, /orgs/1/sites/site005
+
+g, managerUser2034, manager001, /orgs/1/sites/site001
+g, managerUser2034, manager001, /orgs/1/sites/site002
+g, managerUser2034, manager001, /orgs/1/sites/site003
+g, managerUser2034, manager001, /orgs/1/sites/site004
+g, managerUser2034, manager001, /orgs/1/sites/site005
+
+g, managerUser2035, manager001, /orgs/1/sites/site001
+g, managerUser2035, manager001, /orgs/1/sites/site002
+g, managerUser2035, manager001, /orgs/1/sites/site003
+g, managerUser2035, manager001, /orgs/1/sites/site004
+g, managerUser2035, manager001, /orgs/1/sites/site005
+
+g, managerUser2036, manager001, /orgs/1/sites/site001
+g, managerUser2036, manager001, /orgs/1/sites/site002
+g, managerUser2036, manager001, /orgs/1/sites/site003
+g, managerUser2036, manager001, /orgs/1/sites/site004
+g, managerUser2036, manager001, /orgs/1/sites/site005
+
+g, managerUser2037, manager001, /orgs/1/sites/site001
+g, managerUser2037, manager001, /orgs/1/sites/site002
+g, managerUser2037, manager001, /orgs/1/sites/site003
+g, managerUser2037, manager001, /orgs/1/sites/site004
+g, managerUser2037, manager001, /orgs/1/sites/site005
+
+g, managerUser2038, manager001, /orgs/1/sites/site001
+g, managerUser2038, manager001, /orgs/1/sites/site002
+g, managerUser2038, manager001, /orgs/1/sites/site003
+g, managerUser2038, manager001, /orgs/1/sites/site004
+g, managerUser2038, manager001, /orgs/1/sites/site005
+
+g, managerUser2039, manager001, /orgs/1/sites/site001
+g, managerUser2039, manager001, /orgs/1/sites/site002
+g, managerUser2039, manager001, /orgs/1/sites/site003
+g, managerUser2039, manager001, /orgs/1/sites/site004
+g, managerUser2039, manager001, /orgs/1/sites/site005
+
+g, managerUser2040, manager001, /orgs/1/sites/site001
+g, managerUser2040, manager001, /orgs/1/sites/site002
+g, managerUser2040, manager001, /orgs/1/sites/site003
+g, managerUser2040, manager001, /orgs/1/sites/site004
+g, managerUser2040, manager001, /orgs/1/sites/site005
+
+g, managerUser2041, manager001, /orgs/1/sites/site001
+g, managerUser2041, manager001, /orgs/1/sites/site002
+g, managerUser2041, manager001, /orgs/1/sites/site003
+g, managerUser2041, manager001, /orgs/1/sites/site004
+g, managerUser2041, manager001, /orgs/1/sites/site005
+
+g, managerUser2042, manager001, /orgs/1/sites/site001
+g, managerUser2042, manager001, /orgs/1/sites/site002
+g, managerUser2042, manager001, /orgs/1/sites/site003
+g, managerUser2042, manager001, /orgs/1/sites/site004
+g, managerUser2042, manager001, /orgs/1/sites/site005
+
+g, managerUser2043, manager001, /orgs/1/sites/site001
+g, managerUser2043, manager001, /orgs/1/sites/site002
+g, managerUser2043, manager001, /orgs/1/sites/site003
+g, managerUser2043, manager001, /orgs/1/sites/site004
+g, managerUser2043, manager001, /orgs/1/sites/site005
+
+g, managerUser2044, manager001, /orgs/1/sites/site001
+g, managerUser2044, manager001, /orgs/1/sites/site002
+g, managerUser2044, manager001, /orgs/1/sites/site003
+g, managerUser2044, manager001, /orgs/1/sites/site004
+g, managerUser2044, manager001, /orgs/1/sites/site005
+
+g, managerUser2045, manager001, /orgs/1/sites/site001
+g, managerUser2045, manager001, /orgs/1/sites/site002
+g, managerUser2045, manager001, /orgs/1/sites/site003
+g, managerUser2045, manager001, /orgs/1/sites/site004
+g, managerUser2045, manager001, /orgs/1/sites/site005
+
+g, managerUser2046, manager001, /orgs/1/sites/site001
+g, managerUser2046, manager001, /orgs/1/sites/site002
+g, managerUser2046, manager001, /orgs/1/sites/site003
+g, managerUser2046, manager001, /orgs/1/sites/site004
+g, managerUser2046, manager001, /orgs/1/sites/site005
+
+g, managerUser2047, manager001, /orgs/1/sites/site001
+g, managerUser2047, manager001, /orgs/1/sites/site002
+g, managerUser2047, manager001, /orgs/1/sites/site003
+g, managerUser2047, manager001, /orgs/1/sites/site004
+g, managerUser2047, manager001, /orgs/1/sites/site005
+
+g, managerUser2048, manager001, /orgs/1/sites/site001
+g, managerUser2048, manager001, /orgs/1/sites/site002
+g, managerUser2048, manager001, /orgs/1/sites/site003
+g, managerUser2048, manager001, /orgs/1/sites/site004
+g, managerUser2048, manager001, /orgs/1/sites/site005
+
+g, managerUser2049, manager001, /orgs/1/sites/site001
+g, managerUser2049, manager001, /orgs/1/sites/site002
+g, managerUser2049, manager001, /orgs/1/sites/site003
+g, managerUser2049, manager001, /orgs/1/sites/site004
+g, managerUser2049, manager001, /orgs/1/sites/site005
+
+g, managerUser2050, manager001, /orgs/1/sites/site001
+g, managerUser2050, manager001, /orgs/1/sites/site002
+g, managerUser2050, manager001, /orgs/1/sites/site003
+g, managerUser2050, manager001, /orgs/1/sites/site004
+g, managerUser2050, manager001, /orgs/1/sites/site005
+
+# Group - customer001, / org1
+g, customerUser1001, customer001, /orgs/1/sites/site001
+g, customerUser1001, customer001, /orgs/1/sites/site002
+g, customerUser1001, customer001, /orgs/1/sites/site003
+g, customerUser1001, customer001, /orgs/1/sites/site004
+g, customerUser1001, customer001, /orgs/1/sites/site005
+
+g, customerUser1001, customer001, /orgs/1/sites/site001
+g, customerUser1001, customer001, /orgs/1/sites/site002
+g, customerUser1001, customer001, /orgs/1/sites/site003
+g, customerUser1001, customer001, /orgs/1/sites/site004
+g, customerUser1001, customer001, /orgs/1/sites/site005
+
+g, customerUser1003, customer001, /orgs/1/sites/site001
+g, customerUser1003, customer001, /orgs/1/sites/site002
+g, customerUser1003, customer001, /orgs/1/sites/site003
+g, customerUser1003, customer001, /orgs/1/sites/site004
+g, customerUser1003, customer001, /orgs/1/sites/site005
+
+g, customerUser1004, customer001, /orgs/1/sites/site001
+g, customerUser1004, customer001, /orgs/1/sites/site002
+g, customerUser1004, customer001, /orgs/1/sites/site003
+g, customerUser1004, customer001, /orgs/1/sites/site004
+g, customerUser1004, customer001, /orgs/1/sites/site005
+
+g, customerUser1005, customer001, /orgs/1/sites/site001
+g, customerUser1005, customer001, /orgs/1/sites/site002
+g, customerUser1005, customer001, /orgs/1/sites/site003
+g, customerUser1005, customer001, /orgs/1/sites/site004
+g, customerUser1005, customer001, /orgs/1/sites/site005
+
+g, customerUser1006, customer001, /orgs/1/sites/site001
+g, customerUser1006, customer001, /orgs/1/sites/site002
+g, customerUser1006, customer001, /orgs/1/sites/site003
+g, customerUser1006, customer001, /orgs/1/sites/site004
+g, customerUser1006, customer001, /orgs/1/sites/site005
+
+g, customerUser1007, customer001, /orgs/1/sites/site001
+g, customerUser1007, customer001, /orgs/1/sites/site002
+g, customerUser1007, customer001, /orgs/1/sites/site003
+g, customerUser1007, customer001, /orgs/1/sites/site004
+g, customerUser1007, customer001, /orgs/1/sites/site005
+
+g, customerUser1008, customer001, /orgs/1/sites/site001
+g, customerUser1008, customer001, /orgs/1/sites/site002
+g, customerUser1008, customer001, /orgs/1/sites/site003
+g, customerUser1008, customer001, /orgs/1/sites/site004
+g, customerUser1008, customer001, /orgs/1/sites/site005
+
+g, customerUser1009, customer001, /orgs/1/sites/site001
+g, customerUser1009, customer001, /orgs/1/sites/site002
+g, customerUser1009, customer001, /orgs/1/sites/site003
+g, customerUser1009, customer001, /orgs/1/sites/site004
+g, customerUser1009, customer001, /orgs/1/sites/site005
+
+g, customerUser1010, customer001, /orgs/1/sites/site001
+g, customerUser1010, customer001, /orgs/1/sites/site002
+g, customerUser1010, customer001, /orgs/1/sites/site003
+g, customerUser1010, customer001, /orgs/1/sites/site004
+g, customerUser1010, customer001, /orgs/1/sites/site005
+
+g, customerUser1011, customer001, /orgs/1/sites/site001
+g, customerUser1011, customer001, /orgs/1/sites/site002
+g, customerUser1011, customer001, /orgs/1/sites/site003
+g, customerUser1011, customer001, /orgs/1/sites/site004
+g, customerUser1011, customer001, /orgs/1/sites/site005
+
+g, customerUser1012, customer001, /orgs/1/sites/site001
+g, customerUser1012, customer001, /orgs/1/sites/site002
+g, customerUser1012, customer001, /orgs/1/sites/site003
+g, customerUser1012, customer001, /orgs/1/sites/site004
+g, customerUser1012, customer001, /orgs/1/sites/site005
+
+g, customerUser1013, customer001, /orgs/1/sites/site001
+g, customerUser1013, customer001, /orgs/1/sites/site002
+g, customerUser1013, customer001, /orgs/1/sites/site003
+g, customerUser1013, customer001, /orgs/1/sites/site004
+g, customerUser1013, customer001, /orgs/1/sites/site005
+
+g, customerUser1014, customer001, /orgs/1/sites/site001
+g, customerUser1014, customer001, /orgs/1/sites/site002
+g, customerUser1014, customer001, /orgs/1/sites/site003
+g, customerUser1014, customer001, /orgs/1/sites/site004
+g, customerUser1014, customer001, /orgs/1/sites/site005
+
+g, customerUser1015, customer001, /orgs/1/sites/site001
+g, customerUser1015, customer001, /orgs/1/sites/site002
+g, customerUser1015, customer001, /orgs/1/sites/site003
+g, customerUser1015, customer001, /orgs/1/sites/site004
+g, customerUser1015, customer001, /orgs/1/sites/site005
+
+g, customerUser1016, customer001, /orgs/1/sites/site001
+g, customerUser1016, customer001, /orgs/1/sites/site002
+g, customerUser1016, customer001, /orgs/1/sites/site003
+g, customerUser1016, customer001, /orgs/1/sites/site004
+g, customerUser1016, customer001, /orgs/1/sites/site005
+
+g, customerUser1017, customer001, /orgs/1/sites/site001
+g, customerUser1017, customer001, /orgs/1/sites/site002
+g, customerUser1017, customer001, /orgs/1/sites/site003
+g, customerUser1017, customer001, /orgs/1/sites/site004
+g, customerUser1017, customer001, /orgs/1/sites/site005
+
+g, customerUser1018, customer001, /orgs/1/sites/site001
+g, customerUser1018, customer001, /orgs/1/sites/site002
+g, customerUser1018, customer001, /orgs/1/sites/site003
+g, customerUser1018, customer001, /orgs/1/sites/site004
+g, customerUser1018, customer001, /orgs/1/sites/site005
+
+g, customerUser1019, customer001, /orgs/1/sites/site001
+g, customerUser1019, customer001, /orgs/1/sites/site002
+g, customerUser1019, customer001, /orgs/1/sites/site003
+g, customerUser1019, customer001, /orgs/1/sites/site004
+g, customerUser1019, customer001, /orgs/1/sites/site005
+
+g, customerUser1020, customer001, /orgs/1/sites/site001
+g, customerUser1020, customer001, /orgs/1/sites/site002
+g, customerUser1020, customer001, /orgs/1/sites/site003
+g, customerUser1020, customer001, /orgs/1/sites/site004
+g, customerUser1020, customer001, /orgs/1/sites/site005
+
+g, customerUser1021, customer001, /orgs/1/sites/site001
+g, customerUser1021, customer001, /orgs/1/sites/site002
+g, customerUser1021, customer001, /orgs/1/sites/site003
+g, customerUser1021, customer001, /orgs/1/sites/site004
+g, customerUser1021, customer001, /orgs/1/sites/site005
+
+g, customerUser1022, customer001, /orgs/1/sites/site001
+g, customerUser1022, customer001, /orgs/1/sites/site002
+g, customerUser1022, customer001, /orgs/1/sites/site003
+g, customerUser1022, customer001, /orgs/1/sites/site004
+g, customerUser1022, customer001, /orgs/1/sites/site005
+
+g, customerUser1023, customer001, /orgs/1/sites/site001
+g, customerUser1023, customer001, /orgs/1/sites/site002
+g, customerUser1023, customer001, /orgs/1/sites/site003
+g, customerUser1023, customer001, /orgs/1/sites/site004
+g, customerUser1023, customer001, /orgs/1/sites/site005
+
+g, customerUser1024, customer001, /orgs/1/sites/site001
+g, customerUser1024, customer001, /orgs/1/sites/site002
+g, customerUser1024, customer001, /orgs/1/sites/site003
+g, customerUser1024, customer001, /orgs/1/sites/site004
+g, customerUser1024, customer001, /orgs/1/sites/site005
+
+g, customerUser1025, customer001, /orgs/1/sites/site001
+g, customerUser1025, customer001, /orgs/1/sites/site002
+g, customerUser1025, customer001, /orgs/1/sites/site003
+g, customerUser1025, customer001, /orgs/1/sites/site004
+g, customerUser1025, customer001, /orgs/1/sites/site005
+
+g, customerUser1026, customer001, /orgs/1/sites/site001
+g, customerUser1026, customer001, /orgs/1/sites/site002
+g, customerUser1026, customer001, /orgs/1/sites/site003
+g, customerUser1026, customer001, /orgs/1/sites/site004
+g, customerUser1026, customer001, /orgs/1/sites/site005
+
+g, customerUser1027, customer001, /orgs/1/sites/site001
+g, customerUser1027, customer001, /orgs/1/sites/site002
+g, customerUser1027, customer001, /orgs/1/sites/site003
+g, customerUser1027, customer001, /orgs/1/sites/site004
+g, customerUser1027, customer001, /orgs/1/sites/site005
+
+g, customerUser1028, customer001, /orgs/1/sites/site001
+g, customerUser1028, customer001, /orgs/1/sites/site002
+g, customerUser1028, customer001, /orgs/1/sites/site003
+g, customerUser1028, customer001, /orgs/1/sites/site004
+g, customerUser1028, customer001, /orgs/1/sites/site005
+
+g, customerUser1029, customer001, /orgs/1/sites/site001
+g, customerUser1029, customer001, /orgs/1/sites/site002
+g, customerUser1029, customer001, /orgs/1/sites/site003
+g, customerUser1029, customer001, /orgs/1/sites/site004
+g, customerUser1029, customer001, /orgs/1/sites/site005
+
+g, customerUser1030, customer001, /orgs/1/sites/site001
+g, customerUser1030, customer001, /orgs/1/sites/site002
+g, customerUser1030, customer001, /orgs/1/sites/site003
+g, customerUser1030, customer001, /orgs/1/sites/site004
+g, customerUser1030, customer001, /orgs/1/sites/site005
+
+g, customerUser1031, customer001, /orgs/1/sites/site001
+g, customerUser1031, customer001, /orgs/1/sites/site002
+g, customerUser1031, customer001, /orgs/1/sites/site003
+g, customerUser1031, customer001, /orgs/1/sites/site004
+g, customerUser1031, customer001, /orgs/1/sites/site005
+
+g, customerUser1032, customer001, /orgs/1/sites/site001
+g, customerUser1032, customer001, /orgs/1/sites/site002
+g, customerUser1032, customer001, /orgs/1/sites/site003
+g, customerUser1032, customer001, /orgs/1/sites/site004
+g, customerUser1032, customer001, /orgs/1/sites/site005
+
+g, customerUser1033, customer001, /orgs/1/sites/site001
+g, customerUser1033, customer001, /orgs/1/sites/site002
+g, customerUser1033, customer001, /orgs/1/sites/site003
+g, customerUser1033, customer001, /orgs/1/sites/site004
+g, customerUser1033, customer001, /orgs/1/sites/site005
+
+g, customerUser1034, customer001, /orgs/1/sites/site001
+g, customerUser1034, customer001, /orgs/1/sites/site002
+g, customerUser1034, customer001, /orgs/1/sites/site003
+g, customerUser1034, customer001, /orgs/1/sites/site004
+g, customerUser1034, customer001, /orgs/1/sites/site005
+
+g, customerUser1035, customer001, /orgs/1/sites/site001
+g, customerUser1035, customer001, /orgs/1/sites/site002
+g, customerUser1035, customer001, /orgs/1/sites/site003
+g, customerUser1035, customer001, /orgs/1/sites/site004
+g, customerUser1035, customer001, /orgs/1/sites/site005
+
+g, customerUser1036, customer001, /orgs/1/sites/site001
+g, customerUser1036, customer001, /orgs/1/sites/site002
+g, customerUser1036, customer001, /orgs/1/sites/site003
+g, customerUser1036, customer001, /orgs/1/sites/site004
+g, customerUser1036, customer001, /orgs/1/sites/site005
+
+g, customerUser1037, customer001, /orgs/1/sites/site001
+g, customerUser1037, customer001, /orgs/1/sites/site002
+g, customerUser1037, customer001, /orgs/1/sites/site003
+g, customerUser1037, customer001, /orgs/1/sites/site004
+g, customerUser1037, customer001, /orgs/1/sites/site005
+
+g, customerUser1038, customer001, /orgs/1/sites/site001
+g, customerUser1038, customer001, /orgs/1/sites/site002
+g, customerUser1038, customer001, /orgs/1/sites/site003
+g, customerUser1038, customer001, /orgs/1/sites/site004
+g, customerUser1038, customer001, /orgs/1/sites/site005
+
+g, customerUser1039, customer001, /orgs/1/sites/site001
+g, customerUser1039, customer001, /orgs/1/sites/site002
+g, customerUser1039, customer001, /orgs/1/sites/site003
+g, customerUser1039, customer001, /orgs/1/sites/site004
+g, customerUser1039, customer001, /orgs/1/sites/site005
+
+g, customerUser1040, customer001, /orgs/1/sites/site001
+g, customerUser1040, customer001, /orgs/1/sites/site002
+g, customerUser1040, customer001, /orgs/1/sites/site003
+g, customerUser1040, customer001, /orgs/1/sites/site004
+g, customerUser1040, customer001, /orgs/1/sites/site005
+
+g, customerUser1041, customer001, /orgs/1/sites/site001
+g, customerUser1041, customer001, /orgs/1/sites/site002
+g, customerUser1041, customer001, /orgs/1/sites/site003
+g, customerUser1041, customer001, /orgs/1/sites/site004
+g, customerUser1041, customer001, /orgs/1/sites/site005
+
+g, customerUser1042, customer001, /orgs/1/sites/site001
+g, customerUser1042, customer001, /orgs/1/sites/site002
+g, customerUser1042, customer001, /orgs/1/sites/site003
+g, customerUser1042, customer001, /orgs/1/sites/site004
+g, customerUser1042, customer001, /orgs/1/sites/site005
+
+g, customerUser1043, customer001, /orgs/1/sites/site001
+g, customerUser1043, customer001, /orgs/1/sites/site002
+g, customerUser1043, customer001, /orgs/1/sites/site003
+g, customerUser1043, customer001, /orgs/1/sites/site004
+g, customerUser1043, customer001, /orgs/1/sites/site005
+
+g, customerUser1044, customer001, /orgs/1/sites/site001
+g, customerUser1044, customer001, /orgs/1/sites/site002
+g, customerUser1044, customer001, /orgs/1/sites/site003
+g, customerUser1044, customer001, /orgs/1/sites/site004
+g, customerUser1044, customer001, /orgs/1/sites/site005
+
+g, customerUser1045, customer001, /orgs/1/sites/site001
+g, customerUser1045, customer001, /orgs/1/sites/site002
+g, customerUser1045, customer001, /orgs/1/sites/site003
+g, customerUser1045, customer001, /orgs/1/sites/site004
+g, customerUser1045, customer001, /orgs/1/sites/site005
+
+g, customerUser1046, customer001, /orgs/1/sites/site001
+g, customerUser1046, customer001, /orgs/1/sites/site002
+g, customerUser1046, customer001, /orgs/1/sites/site003
+g, customerUser1046, customer001, /orgs/1/sites/site004
+g, customerUser1046, customer001, /orgs/1/sites/site005
+
+g, customerUser1047, customer001, /orgs/1/sites/site001
+g, customerUser1047, customer001, /orgs/1/sites/site002
+g, customerUser1047, customer001, /orgs/1/sites/site003
+g, customerUser1047, customer001, /orgs/1/sites/site004
+g, customerUser1047, customer001, /orgs/1/sites/site005
+
+g, customerUser1048, customer001, /orgs/1/sites/site001
+g, customerUser1048, customer001, /orgs/1/sites/site002
+g, customerUser1048, customer001, /orgs/1/sites/site003
+g, customerUser1048, customer001, /orgs/1/sites/site004
+g, customerUser1048, customer001, /orgs/1/sites/site005
+
+g, customerUser1049, customer001, /orgs/1/sites/site001
+g, customerUser1049, customer001, /orgs/1/sites/site002
+g, customerUser1049, customer001, /orgs/1/sites/site003
+g, customerUser1049, customer001, /orgs/1/sites/site004
+g, customerUser1049, customer001, /orgs/1/sites/site005
+
+g, customerUser1050, customer001, /orgs/1/sites/site001
+g, customerUser1050, customer001, /orgs/1/sites/site002
+g, customerUser1050, customer001, /orgs/1/sites/site003
+g, customerUser1050, customer001, /orgs/1/sites/site004
+g, customerUser1050, customer001, /orgs/1/sites/site005
+
+# Group - customer001, / org1
+g, customerUser2001, customer001, /orgs/1/sites/site001
+g, customerUser2001, customer001, /orgs/1/sites/site002
+g, customerUser2001, customer001, /orgs/1/sites/site003
+g, customerUser2001, customer001, /orgs/1/sites/site004
+g, customerUser2001, customer001, /orgs/1/sites/site005
+
+g, customerUser2001, customer001, /orgs/1/sites/site001
+g, customerUser2001, customer001, /orgs/1/sites/site002
+g, customerUser2001, customer001, /orgs/1/sites/site003
+g, customerUser2001, customer001, /orgs/1/sites/site004
+g, customerUser2001, customer001, /orgs/1/sites/site005
+
+g, customerUser2003, customer001, /orgs/1/sites/site001
+g, customerUser2003, customer001, /orgs/1/sites/site002
+g, customerUser2003, customer001, /orgs/1/sites/site003
+g, customerUser2003, customer001, /orgs/1/sites/site004
+g, customerUser2003, customer001, /orgs/1/sites/site005
+
+g, customerUser2004, customer001, /orgs/1/sites/site001
+g, customerUser2004, customer001, /orgs/1/sites/site002
+g, customerUser2004, customer001, /orgs/1/sites/site003
+g, customerUser2004, customer001, /orgs/1/sites/site004
+g, customerUser2004, customer001, /orgs/1/sites/site005
+
+g, customerUser2005, customer001, /orgs/1/sites/site001
+g, customerUser2005, customer001, /orgs/1/sites/site002
+g, customerUser2005, customer001, /orgs/1/sites/site003
+g, customerUser2005, customer001, /orgs/1/sites/site004
+g, customerUser2005, customer001, /orgs/1/sites/site005
+
+g, customerUser2006, customer001, /orgs/1/sites/site001
+g, customerUser2006, customer001, /orgs/1/sites/site002
+g, customerUser2006, customer001, /orgs/1/sites/site003
+g, customerUser2006, customer001, /orgs/1/sites/site004
+g, customerUser2006, customer001, /orgs/1/sites/site005
+
+g, customerUser2007, customer001, /orgs/1/sites/site001
+g, customerUser2007, customer001, /orgs/1/sites/site002
+g, customerUser2007, customer001, /orgs/1/sites/site003
+g, customerUser2007, customer001, /orgs/1/sites/site004
+g, customerUser2007, customer001, /orgs/1/sites/site005
+
+g, customerUser2008, customer001, /orgs/1/sites/site001
+g, customerUser2008, customer001, /orgs/1/sites/site002
+g, customerUser2008, customer001, /orgs/1/sites/site003
+g, customerUser2008, customer001, /orgs/1/sites/site004
+g, customerUser2008, customer001, /orgs/1/sites/site005
+
+g, customerUser2009, customer001, /orgs/1/sites/site001
+g, customerUser2009, customer001, /orgs/1/sites/site002
+g, customerUser2009, customer001, /orgs/1/sites/site003
+g, customerUser2009, customer001, /orgs/1/sites/site004
+g, customerUser2009, customer001, /orgs/1/sites/site005
+
+g, customerUser2010, customer001, /orgs/1/sites/site001
+g, customerUser2010, customer001, /orgs/1/sites/site002
+g, customerUser2010, customer001, /orgs/1/sites/site003
+g, customerUser2010, customer001, /orgs/1/sites/site004
+g, customerUser2010, customer001, /orgs/1/sites/site005
+
+g, customerUser2011, customer001, /orgs/1/sites/site001
+g, customerUser2011, customer001, /orgs/1/sites/site002
+g, customerUser2011, customer001, /orgs/1/sites/site003
+g, customerUser2011, customer001, /orgs/1/sites/site004
+g, customerUser2011, customer001, /orgs/1/sites/site005
+
+g, customerUser2012, customer001, /orgs/1/sites/site001
+g, customerUser2012, customer001, /orgs/1/sites/site002
+g, customerUser2012, customer001, /orgs/1/sites/site003
+g, customerUser2012, customer001, /orgs/1/sites/site004
+g, customerUser2012, customer001, /orgs/1/sites/site005
+
+g, customerUser2013, customer001, /orgs/1/sites/site001
+g, customerUser2013, customer001, /orgs/1/sites/site002
+g, customerUser2013, customer001, /orgs/1/sites/site003
+g, customerUser2013, customer001, /orgs/1/sites/site004
+g, customerUser2013, customer001, /orgs/1/sites/site005
+
+g, customerUser2014, customer001, /orgs/1/sites/site001
+g, customerUser2014, customer001, /orgs/1/sites/site002
+g, customerUser2014, customer001, /orgs/1/sites/site003
+g, customerUser2014, customer001, /orgs/1/sites/site004
+g, customerUser2014, customer001, /orgs/1/sites/site005
+
+g, customerUser2015, customer001, /orgs/1/sites/site001
+g, customerUser2015, customer001, /orgs/1/sites/site002
+g, customerUser2015, customer001, /orgs/1/sites/site003
+g, customerUser2015, customer001, /orgs/1/sites/site004
+g, customerUser2015, customer001, /orgs/1/sites/site005
+
+g, customerUser2016, customer001, /orgs/1/sites/site001
+g, customerUser2016, customer001, /orgs/1/sites/site002
+g, customerUser2016, customer001, /orgs/1/sites/site003
+g, customerUser2016, customer001, /orgs/1/sites/site004
+g, customerUser2016, customer001, /orgs/1/sites/site005
+
+g, customerUser2017, customer001, /orgs/1/sites/site001
+g, customerUser2017, customer001, /orgs/1/sites/site002
+g, customerUser2017, customer001, /orgs/1/sites/site003
+g, customerUser2017, customer001, /orgs/1/sites/site004
+g, customerUser2017, customer001, /orgs/1/sites/site005
+
+g, customerUser2018, customer001, /orgs/1/sites/site001
+g, customerUser2018, customer001, /orgs/1/sites/site002
+g, customerUser2018, customer001, /orgs/1/sites/site003
+g, customerUser2018, customer001, /orgs/1/sites/site004
+g, customerUser2018, customer001, /orgs/1/sites/site005
+
+g, customerUser2019, customer001, /orgs/1/sites/site001
+g, customerUser2019, customer001, /orgs/1/sites/site002
+g, customerUser2019, customer001, /orgs/1/sites/site003
+g, customerUser2019, customer001, /orgs/1/sites/site004
+g, customerUser2019, customer001, /orgs/1/sites/site005
+
+g, customerUser2020, customer001, /orgs/1/sites/site001
+g, customerUser2020, customer001, /orgs/1/sites/site002
+g, customerUser2020, customer001, /orgs/1/sites/site003
+g, customerUser2020, customer001, /orgs/1/sites/site004
+g, customerUser2020, customer001, /orgs/1/sites/site005
+
+g, customerUser2021, customer001, /orgs/1/sites/site001
+g, customerUser2021, customer001, /orgs/1/sites/site002
+g, customerUser2021, customer001, /orgs/1/sites/site003
+g, customerUser2021, customer001, /orgs/1/sites/site004
+g, customerUser2021, customer001, /orgs/1/sites/site005
+
+g, customerUser2022, customer001, /orgs/1/sites/site001
+g, customerUser2022, customer001, /orgs/1/sites/site002
+g, customerUser2022, customer001, /orgs/1/sites/site003
+g, customerUser2022, customer001, /orgs/1/sites/site004
+g, customerUser2022, customer001, /orgs/1/sites/site005
+
+g, customerUser2023, customer001, /orgs/1/sites/site001
+g, customerUser2023, customer001, /orgs/1/sites/site002
+g, customerUser2023, customer001, /orgs/1/sites/site003
+g, customerUser2023, customer001, /orgs/1/sites/site004
+g, customerUser2023, customer001, /orgs/1/sites/site005
+
+g, customerUser2024, customer001, /orgs/1/sites/site001
+g, customerUser2024, customer001, /orgs/1/sites/site002
+g, customerUser2024, customer001, /orgs/1/sites/site003
+g, customerUser2024, customer001, /orgs/1/sites/site004
+g, customerUser2024, customer001, /orgs/1/sites/site005
+
+g, customerUser2025, customer001, /orgs/1/sites/site001
+g, customerUser2025, customer001, /orgs/1/sites/site002
+g, customerUser2025, customer001, /orgs/1/sites/site003
+g, customerUser2025, customer001, /orgs/1/sites/site004
+g, customerUser2025, customer001, /orgs/1/sites/site005
+
+g, customerUser2026, customer001, /orgs/1/sites/site001
+g, customerUser2026, customer001, /orgs/1/sites/site002
+g, customerUser2026, customer001, /orgs/1/sites/site003
+g, customerUser2026, customer001, /orgs/1/sites/site004
+g, customerUser2026, customer001, /orgs/1/sites/site005
+
+g, customerUser2027, customer001, /orgs/1/sites/site001
+g, customerUser2027, customer001, /orgs/1/sites/site002
+g, customerUser2027, customer001, /orgs/1/sites/site003
+g, customerUser2027, customer001, /orgs/1/sites/site004
+g, customerUser2027, customer001, /orgs/1/sites/site005
+
+g, customerUser2028, customer001, /orgs/1/sites/site001
+g, customerUser2028, customer001, /orgs/1/sites/site002
+g, customerUser2028, customer001, /orgs/1/sites/site003
+g, customerUser2028, customer001, /orgs/1/sites/site004
+g, customerUser2028, customer001, /orgs/1/sites/site005
+
+g, customerUser2029, customer001, /orgs/1/sites/site001
+g, customerUser2029, customer001, /orgs/1/sites/site002
+g, customerUser2029, customer001, /orgs/1/sites/site003
+g, customerUser2029, customer001, /orgs/1/sites/site004
+g, customerUser2029, customer001, /orgs/1/sites/site005
+
+g, customerUser2030, customer001, /orgs/1/sites/site001
+g, customerUser2030, customer001, /orgs/1/sites/site002
+g, customerUser2030, customer001, /orgs/1/sites/site003
+g, customerUser2030, customer001, /orgs/1/sites/site004
+g, customerUser2030, customer001, /orgs/1/sites/site005
+
+g, customerUser2031, customer001, /orgs/1/sites/site001
+g, customerUser2031, customer001, /orgs/1/sites/site002
+g, customerUser2031, customer001, /orgs/1/sites/site003
+g, customerUser2031, customer001, /orgs/1/sites/site004
+g, customerUser2031, customer001, /orgs/1/sites/site005
+
+g, customerUser2032, customer001, /orgs/1/sites/site001
+g, customerUser2032, customer001, /orgs/1/sites/site002
+g, customerUser2032, customer001, /orgs/1/sites/site003
+g, customerUser2032, customer001, /orgs/1/sites/site004
+g, customerUser2032, customer001, /orgs/1/sites/site005
+
+g, customerUser2033, customer001, /orgs/1/sites/site001
+g, customerUser2033, customer001, /orgs/1/sites/site002
+g, customerUser2033, customer001, /orgs/1/sites/site003
+g, customerUser2033, customer001, /orgs/1/sites/site004
+g, customerUser2033, customer001, /orgs/1/sites/site005
+
+g, customerUser2034, customer001, /orgs/1/sites/site001
+g, customerUser2034, customer001, /orgs/1/sites/site002
+g, customerUser2034, customer001, /orgs/1/sites/site003
+g, customerUser2034, customer001, /orgs/1/sites/site004
+g, customerUser2034, customer001, /orgs/1/sites/site005
+
+g, customerUser2035, customer001, /orgs/1/sites/site001
+g, customerUser2035, customer001, /orgs/1/sites/site002
+g, customerUser2035, customer001, /orgs/1/sites/site003
+g, customerUser2035, customer001, /orgs/1/sites/site004
+g, customerUser2035, customer001, /orgs/1/sites/site005
+
+g, customerUser2036, customer001, /orgs/1/sites/site001
+g, customerUser2036, customer001, /orgs/1/sites/site002
+g, customerUser2036, customer001, /orgs/1/sites/site003
+g, customerUser2036, customer001, /orgs/1/sites/site004
+g, customerUser2036, customer001, /orgs/1/sites/site005
+
+g, customerUser2037, customer001, /orgs/1/sites/site001
+g, customerUser2037, customer001, /orgs/1/sites/site002
+g, customerUser2037, customer001, /orgs/1/sites/site003
+g, customerUser2037, customer001, /orgs/1/sites/site004
+g, customerUser2037, customer001, /orgs/1/sites/site005
+
+g, customerUser2038, customer001, /orgs/1/sites/site001
+g, customerUser2038, customer001, /orgs/1/sites/site002
+g, customerUser2038, customer001, /orgs/1/sites/site003
+g, customerUser2038, customer001, /orgs/1/sites/site004
+g, customerUser2038, customer001, /orgs/1/sites/site005
+
+g, customerUser2039, customer001, /orgs/1/sites/site001
+g, customerUser2039, customer001, /orgs/1/sites/site002
+g, customerUser2039, customer001, /orgs/1/sites/site003
+g, customerUser2039, customer001, /orgs/1/sites/site004
+g, customerUser2039, customer001, /orgs/1/sites/site005
+
+g, customerUser2040, customer001, /orgs/1/sites/site001
+g, customerUser2040, customer001, /orgs/1/sites/site002
+g, customerUser2040, customer001, /orgs/1/sites/site003
+g, customerUser2040, customer001, /orgs/1/sites/site004
+g, customerUser2040, customer001, /orgs/1/sites/site005
+
+g, customerUser2041, customer001, /orgs/1/sites/site001
+g, customerUser2041, customer001, /orgs/1/sites/site002
+g, customerUser2041, customer001, /orgs/1/sites/site003
+g, customerUser2041, customer001, /orgs/1/sites/site004
+g, customerUser2041, customer001, /orgs/1/sites/site005
+
+g, customerUser2042, customer001, /orgs/1/sites/site001
+g, customerUser2042, customer001, /orgs/1/sites/site002
+g, customerUser2042, customer001, /orgs/1/sites/site003
+g, customerUser2042, customer001, /orgs/1/sites/site004
+g, customerUser2042, customer001, /orgs/1/sites/site005
+
+g, customerUser2043, customer001, /orgs/1/sites/site001
+g, customerUser2043, customer001, /orgs/1/sites/site002
+g, customerUser2043, customer001, /orgs/1/sites/site003
+g, customerUser2043, customer001, /orgs/1/sites/site004
+g, customerUser2043, customer001, /orgs/1/sites/site005
+
+g, customerUser2044, customer001, /orgs/1/sites/site001
+g, customerUser2044, customer001, /orgs/1/sites/site002
+g, customerUser2044, customer001, /orgs/1/sites/site003
+g, customerUser2044, customer001, /orgs/1/sites/site004
+g, customerUser2044, customer001, /orgs/1/sites/site005
+
+g, customerUser2045, customer001, /orgs/1/sites/site001
+g, customerUser2045, customer001, /orgs/1/sites/site002
+g, customerUser2045, customer001, /orgs/1/sites/site003
+g, customerUser2045, customer001, /orgs/1/sites/site004
+g, customerUser2045, customer001, /orgs/1/sites/site005
+
+g, customerUser2046, customer001, /orgs/1/sites/site001
+g, customerUser2046, customer001, /orgs/1/sites/site002
+g, customerUser2046, customer001, /orgs/1/sites/site003
+g, customerUser2046, customer001, /orgs/1/sites/site004
+g, customerUser2046, customer001, /orgs/1/sites/site005
+
+g, customerUser2047, customer001, /orgs/1/sites/site001
+g, customerUser2047, customer001, /orgs/1/sites/site002
+g, customerUser2047, customer001, /orgs/1/sites/site003
+g, customerUser2047, customer001, /orgs/1/sites/site004
+g, customerUser2047, customer001, /orgs/1/sites/site005
+
+g, customerUser2048, customer001, /orgs/1/sites/site001
+g, customerUser2048, customer001, /orgs/1/sites/site002
+g, customerUser2048, customer001, /orgs/1/sites/site003
+g, customerUser2048, customer001, /orgs/1/sites/site004
+g, customerUser2048, customer001, /orgs/1/sites/site005
+
+g, customerUser2049, customer001, /orgs/1/sites/site001
+g, customerUser2049, customer001, /orgs/1/sites/site002
+g, customerUser2049, customer001, /orgs/1/sites/site003
+g, customerUser2049, customer001, /orgs/1/sites/site004
+g, customerUser2049, customer001, /orgs/1/sites/site005
+
+g, customerUser2050, customer001, /orgs/1/sites/site001
+g, customerUser2050, customer001, /orgs/1/sites/site002
+g, customerUser2050, customer001, /orgs/1/sites/site003
+g, customerUser2050, customer001, /orgs/1/sites/site004
+g, customerUser2050, customer001, /orgs/1/sites/site005
+
+# Group - staff001, / org2
+g, staffUser1001, staff001, /orgs/2/sites/site001
+g, staffUser1001, staff001, /orgs/2/sites/site002
+g, staffUser1001, staff001, /orgs/2/sites/site003
+g, staffUser1001, staff001, /orgs/2/sites/site004
+g, staffUser1001, staff001, /orgs/2/sites/site005
+
+g, staffUser1001, staff001, /orgs/2/sites/site001
+g, staffUser1001, staff001, /orgs/2/sites/site002
+g, staffUser1001, staff001, /orgs/2/sites/site003
+g, staffUser1001, staff001, /orgs/2/sites/site004
+g, staffUser1001, staff001, /orgs/2/sites/site005
+
+g, staffUser1003, staff001, /orgs/2/sites/site001
+g, staffUser1003, staff001, /orgs/2/sites/site002
+g, staffUser1003, staff001, /orgs/2/sites/site003
+g, staffUser1003, staff001, /orgs/2/sites/site004
+g, staffUser1003, staff001, /orgs/2/sites/site005
+
+g, staffUser1004, staff001, /orgs/2/sites/site001
+g, staffUser1004, staff001, /orgs/2/sites/site002
+g, staffUser1004, staff001, /orgs/2/sites/site003
+g, staffUser1004, staff001, /orgs/2/sites/site004
+g, staffUser1004, staff001, /orgs/2/sites/site005
+
+g, staffUser1005, staff001, /orgs/2/sites/site001
+g, staffUser1005, staff001, /orgs/2/sites/site002
+g, staffUser1005, staff001, /orgs/2/sites/site003
+g, staffUser1005, staff001, /orgs/2/sites/site004
+g, staffUser1005, staff001, /orgs/2/sites/site005
+
+g, staffUser1006, staff001, /orgs/2/sites/site001
+g, staffUser1006, staff001, /orgs/2/sites/site002
+g, staffUser1006, staff001, /orgs/2/sites/site003
+g, staffUser1006, staff001, /orgs/2/sites/site004
+g, staffUser1006, staff001, /orgs/2/sites/site005
+
+g, staffUser1007, staff001, /orgs/2/sites/site001
+g, staffUser1007, staff001, /orgs/2/sites/site002
+g, staffUser1007, staff001, /orgs/2/sites/site003
+g, staffUser1007, staff001, /orgs/2/sites/site004
+g, staffUser1007, staff001, /orgs/2/sites/site005
+
+g, staffUser1008, staff001, /orgs/2/sites/site001
+g, staffUser1008, staff001, /orgs/2/sites/site002
+g, staffUser1008, staff001, /orgs/2/sites/site003
+g, staffUser1008, staff001, /orgs/2/sites/site004
+g, staffUser1008, staff001, /orgs/2/sites/site005
+
+g, staffUser1009, staff001, /orgs/2/sites/site001
+g, staffUser1009, staff001, /orgs/2/sites/site002
+g, staffUser1009, staff001, /orgs/2/sites/site003
+g, staffUser1009, staff001, /orgs/2/sites/site004
+g, staffUser1009, staff001, /orgs/2/sites/site005
+
+g, staffUser1010, staff001, /orgs/2/sites/site001
+g, staffUser1010, staff001, /orgs/2/sites/site002
+g, staffUser1010, staff001, /orgs/2/sites/site003
+g, staffUser1010, staff001, /orgs/2/sites/site004
+g, staffUser1010, staff001, /orgs/2/sites/site005
+
+g, staffUser1011, staff001, /orgs/2/sites/site001
+g, staffUser1011, staff001, /orgs/2/sites/site002
+g, staffUser1011, staff001, /orgs/2/sites/site003
+g, staffUser1011, staff001, /orgs/2/sites/site004
+g, staffUser1011, staff001, /orgs/2/sites/site005
+
+g, staffUser1012, staff001, /orgs/2/sites/site001
+g, staffUser1012, staff001, /orgs/2/sites/site002
+g, staffUser1012, staff001, /orgs/2/sites/site003
+g, staffUser1012, staff001, /orgs/2/sites/site004
+g, staffUser1012, staff001, /orgs/2/sites/site005
+
+g, staffUser1013, staff001, /orgs/2/sites/site001
+g, staffUser1013, staff001, /orgs/2/sites/site002
+g, staffUser1013, staff001, /orgs/2/sites/site003
+g, staffUser1013, staff001, /orgs/2/sites/site004
+g, staffUser1013, staff001, /orgs/2/sites/site005
+
+g, staffUser1014, staff001, /orgs/2/sites/site001
+g, staffUser1014, staff001, /orgs/2/sites/site002
+g, staffUser1014, staff001, /orgs/2/sites/site003
+g, staffUser1014, staff001, /orgs/2/sites/site004
+g, staffUser1014, staff001, /orgs/2/sites/site005
+
+g, staffUser1015, staff001, /orgs/2/sites/site001
+g, staffUser1015, staff001, /orgs/2/sites/site002
+g, staffUser1015, staff001, /orgs/2/sites/site003
+g, staffUser1015, staff001, /orgs/2/sites/site004
+g, staffUser1015, staff001, /orgs/2/sites/site005
+
+g, staffUser1016, staff001, /orgs/2/sites/site001
+g, staffUser1016, staff001, /orgs/2/sites/site002
+g, staffUser1016, staff001, /orgs/2/sites/site003
+g, staffUser1016, staff001, /orgs/2/sites/site004
+g, staffUser1016, staff001, /orgs/2/sites/site005
+
+g, staffUser1017, staff001, /orgs/2/sites/site001
+g, staffUser1017, staff001, /orgs/2/sites/site002
+g, staffUser1017, staff001, /orgs/2/sites/site003
+g, staffUser1017, staff001, /orgs/2/sites/site004
+g, staffUser1017, staff001, /orgs/2/sites/site005
+
+g, staffUser1018, staff001, /orgs/2/sites/site001
+g, staffUser1018, staff001, /orgs/2/sites/site002
+g, staffUser1018, staff001, /orgs/2/sites/site003
+g, staffUser1018, staff001, /orgs/2/sites/site004
+g, staffUser1018, staff001, /orgs/2/sites/site005
+
+g, staffUser1019, staff001, /orgs/2/sites/site001
+g, staffUser1019, staff001, /orgs/2/sites/site002
+g, staffUser1019, staff001, /orgs/2/sites/site003
+g, staffUser1019, staff001, /orgs/2/sites/site004
+g, staffUser1019, staff001, /orgs/2/sites/site005
+
+g, staffUser1020, staff001, /orgs/2/sites/site001
+g, staffUser1020, staff001, /orgs/2/sites/site002
+g, staffUser1020, staff001, /orgs/2/sites/site003
+g, staffUser1020, staff001, /orgs/2/sites/site004
+g, staffUser1020, staff001, /orgs/2/sites/site005
+
+g, staffUser1021, staff001, /orgs/2/sites/site001
+g, staffUser1021, staff001, /orgs/2/sites/site002
+g, staffUser1021, staff001, /orgs/2/sites/site003
+g, staffUser1021, staff001, /orgs/2/sites/site004
+g, staffUser1021, staff001, /orgs/2/sites/site005
+
+g, staffUser1022, staff001, /orgs/2/sites/site001
+g, staffUser1022, staff001, /orgs/2/sites/site002
+g, staffUser1022, staff001, /orgs/2/sites/site003
+g, staffUser1022, staff001, /orgs/2/sites/site004
+g, staffUser1022, staff001, /orgs/2/sites/site005
+
+g, staffUser1023, staff001, /orgs/2/sites/site001
+g, staffUser1023, staff001, /orgs/2/sites/site002
+g, staffUser1023, staff001, /orgs/2/sites/site003
+g, staffUser1023, staff001, /orgs/2/sites/site004
+g, staffUser1023, staff001, /orgs/2/sites/site005
+
+g, staffUser1024, staff001, /orgs/2/sites/site001
+g, staffUser1024, staff001, /orgs/2/sites/site002
+g, staffUser1024, staff001, /orgs/2/sites/site003
+g, staffUser1024, staff001, /orgs/2/sites/site004
+g, staffUser1024, staff001, /orgs/2/sites/site005
+
+g, staffUser1025, staff001, /orgs/2/sites/site001
+g, staffUser1025, staff001, /orgs/2/sites/site002
+g, staffUser1025, staff001, /orgs/2/sites/site003
+g, staffUser1025, staff001, /orgs/2/sites/site004
+g, staffUser1025, staff001, /orgs/2/sites/site005
+
+g, staffUser1026, staff001, /orgs/2/sites/site001
+g, staffUser1026, staff001, /orgs/2/sites/site002
+g, staffUser1026, staff001, /orgs/2/sites/site003
+g, staffUser1026, staff001, /orgs/2/sites/site004
+g, staffUser1026, staff001, /orgs/2/sites/site005
+
+g, staffUser1027, staff001, /orgs/2/sites/site001
+g, staffUser1027, staff001, /orgs/2/sites/site002
+g, staffUser1027, staff001, /orgs/2/sites/site003
+g, staffUser1027, staff001, /orgs/2/sites/site004
+g, staffUser1027, staff001, /orgs/2/sites/site005
+
+g, staffUser1028, staff001, /orgs/2/sites/site001
+g, staffUser1028, staff001, /orgs/2/sites/site002
+g, staffUser1028, staff001, /orgs/2/sites/site003
+g, staffUser1028, staff001, /orgs/2/sites/site004
+g, staffUser1028, staff001, /orgs/2/sites/site005
+
+g, staffUser1029, staff001, /orgs/2/sites/site001
+g, staffUser1029, staff001, /orgs/2/sites/site002
+g, staffUser1029, staff001, /orgs/2/sites/site003
+g, staffUser1029, staff001, /orgs/2/sites/site004
+g, staffUser1029, staff001, /orgs/2/sites/site005
+
+g, staffUser1030, staff001, /orgs/2/sites/site001
+g, staffUser1030, staff001, /orgs/2/sites/site002
+g, staffUser1030, staff001, /orgs/2/sites/site003
+g, staffUser1030, staff001, /orgs/2/sites/site004
+g, staffUser1030, staff001, /orgs/2/sites/site005
+
+g, staffUser1031, staff001, /orgs/2/sites/site001
+g, staffUser1031, staff001, /orgs/2/sites/site002
+g, staffUser1031, staff001, /orgs/2/sites/site003
+g, staffUser1031, staff001, /orgs/2/sites/site004
+g, staffUser1031, staff001, /orgs/2/sites/site005
+
+g, staffUser1032, staff001, /orgs/2/sites/site001
+g, staffUser1032, staff001, /orgs/2/sites/site002
+g, staffUser1032, staff001, /orgs/2/sites/site003
+g, staffUser1032, staff001, /orgs/2/sites/site004
+g, staffUser1032, staff001, /orgs/2/sites/site005
+
+g, staffUser1033, staff001, /orgs/2/sites/site001
+g, staffUser1033, staff001, /orgs/2/sites/site002
+g, staffUser1033, staff001, /orgs/2/sites/site003
+g, staffUser1033, staff001, /orgs/2/sites/site004
+g, staffUser1033, staff001, /orgs/2/sites/site005
+
+g, staffUser1034, staff001, /orgs/2/sites/site001
+g, staffUser1034, staff001, /orgs/2/sites/site002
+g, staffUser1034, staff001, /orgs/2/sites/site003
+g, staffUser1034, staff001, /orgs/2/sites/site004
+g, staffUser1034, staff001, /orgs/2/sites/site005
+
+g, staffUser1035, staff001, /orgs/2/sites/site001
+g, staffUser1035, staff001, /orgs/2/sites/site002
+g, staffUser1035, staff001, /orgs/2/sites/site003
+g, staffUser1035, staff001, /orgs/2/sites/site004
+g, staffUser1035, staff001, /orgs/2/sites/site005
+
+g, staffUser1036, staff001, /orgs/2/sites/site001
+g, staffUser1036, staff001, /orgs/2/sites/site002
+g, staffUser1036, staff001, /orgs/2/sites/site003
+g, staffUser1036, staff001, /orgs/2/sites/site004
+g, staffUser1036, staff001, /orgs/2/sites/site005
+
+g, staffUser1037, staff001, /orgs/2/sites/site001
+g, staffUser1037, staff001, /orgs/2/sites/site002
+g, staffUser1037, staff001, /orgs/2/sites/site003
+g, staffUser1037, staff001, /orgs/2/sites/site004
+g, staffUser1037, staff001, /orgs/2/sites/site005
+
+g, staffUser1038, staff001, /orgs/2/sites/site001
+g, staffUser1038, staff001, /orgs/2/sites/site002
+g, staffUser1038, staff001, /orgs/2/sites/site003
+g, staffUser1038, staff001, /orgs/2/sites/site004
+g, staffUser1038, staff001, /orgs/2/sites/site005
+
+g, staffUser1039, staff001, /orgs/2/sites/site001
+g, staffUser1039, staff001, /orgs/2/sites/site002
+g, staffUser1039, staff001, /orgs/2/sites/site003
+g, staffUser1039, staff001, /orgs/2/sites/site004
+g, staffUser1039, staff001, /orgs/2/sites/site005
+
+g, staffUser1040, staff001, /orgs/2/sites/site001
+g, staffUser1040, staff001, /orgs/2/sites/site002
+g, staffUser1040, staff001, /orgs/2/sites/site003
+g, staffUser1040, staff001, /orgs/2/sites/site004
+g, staffUser1040, staff001, /orgs/2/sites/site005
+
+g, staffUser1041, staff001, /orgs/2/sites/site001
+g, staffUser1041, staff001, /orgs/2/sites/site002
+g, staffUser1041, staff001, /orgs/2/sites/site003
+g, staffUser1041, staff001, /orgs/2/sites/site004
+g, staffUser1041, staff001, /orgs/2/sites/site005
+
+g, staffUser1042, staff001, /orgs/2/sites/site001
+g, staffUser1042, staff001, /orgs/2/sites/site002
+g, staffUser1042, staff001, /orgs/2/sites/site003
+g, staffUser1042, staff001, /orgs/2/sites/site004
+g, staffUser1042, staff001, /orgs/2/sites/site005
+
+g, staffUser1043, staff001, /orgs/2/sites/site001
+g, staffUser1043, staff001, /orgs/2/sites/site002
+g, staffUser1043, staff001, /orgs/2/sites/site003
+g, staffUser1043, staff001, /orgs/2/sites/site004
+g, staffUser1043, staff001, /orgs/2/sites/site005
+
+g, staffUser1044, staff001, /orgs/2/sites/site001
+g, staffUser1044, staff001, /orgs/2/sites/site002
+g, staffUser1044, staff001, /orgs/2/sites/site003
+g, staffUser1044, staff001, /orgs/2/sites/site004
+g, staffUser1044, staff001, /orgs/2/sites/site005
+
+g, staffUser1045, staff001, /orgs/2/sites/site001
+g, staffUser1045, staff001, /orgs/2/sites/site002
+g, staffUser1045, staff001, /orgs/2/sites/site003
+g, staffUser1045, staff001, /orgs/2/sites/site004
+g, staffUser1045, staff001, /orgs/2/sites/site005
+
+g, staffUser1046, staff001, /orgs/2/sites/site001
+g, staffUser1046, staff001, /orgs/2/sites/site002
+g, staffUser1046, staff001, /orgs/2/sites/site003
+g, staffUser1046, staff001, /orgs/2/sites/site004
+g, staffUser1046, staff001, /orgs/2/sites/site005
+
+g, staffUser1047, staff001, /orgs/2/sites/site001
+g, staffUser1047, staff001, /orgs/2/sites/site002
+g, staffUser1047, staff001, /orgs/2/sites/site003
+g, staffUser1047, staff001, /orgs/2/sites/site004
+g, staffUser1047, staff001, /orgs/2/sites/site005
+
+g, staffUser1048, staff001, /orgs/2/sites/site001
+g, staffUser1048, staff001, /orgs/2/sites/site002
+g, staffUser1048, staff001, /orgs/2/sites/site003
+g, staffUser1048, staff001, /orgs/2/sites/site004
+g, staffUser1048, staff001, /orgs/2/sites/site005
+
+g, staffUser1049, staff001, /orgs/2/sites/site001
+g, staffUser1049, staff001, /orgs/2/sites/site002
+g, staffUser1049, staff001, /orgs/2/sites/site003
+g, staffUser1049, staff001, /orgs/2/sites/site004
+g, staffUser1049, staff001, /orgs/2/sites/site005
+
+g, staffUser1050, staff001, /orgs/2/sites/site001
+g, staffUser1050, staff001, /orgs/2/sites/site002
+g, staffUser1050, staff001, /orgs/2/sites/site003
+g, staffUser1050, staff001, /orgs/2/sites/site004
+g, staffUser1050, staff001, /orgs/2/sites/site005
+
+# Group - staff001, / org2
+g, staffUser2001, staff001, /orgs/2/sites/site001
+g, staffUser2001, staff001, /orgs/2/sites/site002
+g, staffUser2001, staff001, /orgs/2/sites/site003
+g, staffUser2001, staff001, /orgs/2/sites/site004
+g, staffUser2001, staff001, /orgs/2/sites/site005
+
+g, staffUser2001, staff001, /orgs/2/sites/site001
+g, staffUser2001, staff001, /orgs/2/sites/site002
+g, staffUser2001, staff001, /orgs/2/sites/site003
+g, staffUser2001, staff001, /orgs/2/sites/site004
+g, staffUser2001, staff001, /orgs/2/sites/site005
+
+g, staffUser2003, staff001, /orgs/2/sites/site001
+g, staffUser2003, staff001, /orgs/2/sites/site002
+g, staffUser2003, staff001, /orgs/2/sites/site003
+g, staffUser2003, staff001, /orgs/2/sites/site004
+g, staffUser2003, staff001, /orgs/2/sites/site005
+
+g, staffUser2004, staff001, /orgs/2/sites/site001
+g, staffUser2004, staff001, /orgs/2/sites/site002
+g, staffUser2004, staff001, /orgs/2/sites/site003
+g, staffUser2004, staff001, /orgs/2/sites/site004
+g, staffUser2004, staff001, /orgs/2/sites/site005
+
+g, staffUser2005, staff001, /orgs/2/sites/site001
+g, staffUser2005, staff001, /orgs/2/sites/site002
+g, staffUser2005, staff001, /orgs/2/sites/site003
+g, staffUser2005, staff001, /orgs/2/sites/site004
+g, staffUser2005, staff001, /orgs/2/sites/site005
+
+g, staffUser2006, staff001, /orgs/2/sites/site001
+g, staffUser2006, staff001, /orgs/2/sites/site002
+g, staffUser2006, staff001, /orgs/2/sites/site003
+g, staffUser2006, staff001, /orgs/2/sites/site004
+g, staffUser2006, staff001, /orgs/2/sites/site005
+
+g, staffUser2007, staff001, /orgs/2/sites/site001
+g, staffUser2007, staff001, /orgs/2/sites/site002
+g, staffUser2007, staff001, /orgs/2/sites/site003
+g, staffUser2007, staff001, /orgs/2/sites/site004
+g, staffUser2007, staff001, /orgs/2/sites/site005
+
+g, staffUser2008, staff001, /orgs/2/sites/site001
+g, staffUser2008, staff001, /orgs/2/sites/site002
+g, staffUser2008, staff001, /orgs/2/sites/site003
+g, staffUser2008, staff001, /orgs/2/sites/site004
+g, staffUser2008, staff001, /orgs/2/sites/site005
+
+g, staffUser2009, staff001, /orgs/2/sites/site001
+g, staffUser2009, staff001, /orgs/2/sites/site002
+g, staffUser2009, staff001, /orgs/2/sites/site003
+g, staffUser2009, staff001, /orgs/2/sites/site004
+g, staffUser2009, staff001, /orgs/2/sites/site005
+
+g, staffUser2010, staff001, /orgs/2/sites/site001
+g, staffUser2010, staff001, /orgs/2/sites/site002
+g, staffUser2010, staff001, /orgs/2/sites/site003
+g, staffUser2010, staff001, /orgs/2/sites/site004
+g, staffUser2010, staff001, /orgs/2/sites/site005
+
+g, staffUser2011, staff001, /orgs/2/sites/site001
+g, staffUser2011, staff001, /orgs/2/sites/site002
+g, staffUser2011, staff001, /orgs/2/sites/site003
+g, staffUser2011, staff001, /orgs/2/sites/site004
+g, staffUser2011, staff001, /orgs/2/sites/site005
+
+g, staffUser2012, staff001, /orgs/2/sites/site001
+g, staffUser2012, staff001, /orgs/2/sites/site002
+g, staffUser2012, staff001, /orgs/2/sites/site003
+g, staffUser2012, staff001, /orgs/2/sites/site004
+g, staffUser2012, staff001, /orgs/2/sites/site005
+
+g, staffUser2013, staff001, /orgs/2/sites/site001
+g, staffUser2013, staff001, /orgs/2/sites/site002
+g, staffUser2013, staff001, /orgs/2/sites/site003
+g, staffUser2013, staff001, /orgs/2/sites/site004
+g, staffUser2013, staff001, /orgs/2/sites/site005
+
+g, staffUser2014, staff001, /orgs/2/sites/site001
+g, staffUser2014, staff001, /orgs/2/sites/site002
+g, staffUser2014, staff001, /orgs/2/sites/site003
+g, staffUser2014, staff001, /orgs/2/sites/site004
+g, staffUser2014, staff001, /orgs/2/sites/site005
+
+g, staffUser2015, staff001, /orgs/2/sites/site001
+g, staffUser2015, staff001, /orgs/2/sites/site002
+g, staffUser2015, staff001, /orgs/2/sites/site003
+g, staffUser2015, staff001, /orgs/2/sites/site004
+g, staffUser2015, staff001, /orgs/2/sites/site005
+
+g, staffUser2016, staff001, /orgs/2/sites/site001
+g, staffUser2016, staff001, /orgs/2/sites/site002
+g, staffUser2016, staff001, /orgs/2/sites/site003
+g, staffUser2016, staff001, /orgs/2/sites/site004
+g, staffUser2016, staff001, /orgs/2/sites/site005
+
+g, staffUser2017, staff001, /orgs/2/sites/site001
+g, staffUser2017, staff001, /orgs/2/sites/site002
+g, staffUser2017, staff001, /orgs/2/sites/site003
+g, staffUser2017, staff001, /orgs/2/sites/site004
+g, staffUser2017, staff001, /orgs/2/sites/site005
+
+g, staffUser2018, staff001, /orgs/2/sites/site001
+g, staffUser2018, staff001, /orgs/2/sites/site002
+g, staffUser2018, staff001, /orgs/2/sites/site003
+g, staffUser2018, staff001, /orgs/2/sites/site004
+g, staffUser2018, staff001, /orgs/2/sites/site005
+
+g, staffUser2019, staff001, /orgs/2/sites/site001
+g, staffUser2019, staff001, /orgs/2/sites/site002
+g, staffUser2019, staff001, /orgs/2/sites/site003
+g, staffUser2019, staff001, /orgs/2/sites/site004
+g, staffUser2019, staff001, /orgs/2/sites/site005
+
+g, staffUser2020, staff001, /orgs/2/sites/site001
+g, staffUser2020, staff001, /orgs/2/sites/site002
+g, staffUser2020, staff001, /orgs/2/sites/site003
+g, staffUser2020, staff001, /orgs/2/sites/site004
+g, staffUser2020, staff001, /orgs/2/sites/site005
+
+g, staffUser2021, staff001, /orgs/2/sites/site001
+g, staffUser2021, staff001, /orgs/2/sites/site002
+g, staffUser2021, staff001, /orgs/2/sites/site003
+g, staffUser2021, staff001, /orgs/2/sites/site004
+g, staffUser2021, staff001, /orgs/2/sites/site005
+
+g, staffUser2022, staff001, /orgs/2/sites/site001
+g, staffUser2022, staff001, /orgs/2/sites/site002
+g, staffUser2022, staff001, /orgs/2/sites/site003
+g, staffUser2022, staff001, /orgs/2/sites/site004
+g, staffUser2022, staff001, /orgs/2/sites/site005
+
+g, staffUser2023, staff001, /orgs/2/sites/site001
+g, staffUser2023, staff001, /orgs/2/sites/site002
+g, staffUser2023, staff001, /orgs/2/sites/site003
+g, staffUser2023, staff001, /orgs/2/sites/site004
+g, staffUser2023, staff001, /orgs/2/sites/site005
+
+g, staffUser2024, staff001, /orgs/2/sites/site001
+g, staffUser2024, staff001, /orgs/2/sites/site002
+g, staffUser2024, staff001, /orgs/2/sites/site003
+g, staffUser2024, staff001, /orgs/2/sites/site004
+g, staffUser2024, staff001, /orgs/2/sites/site005
+
+g, staffUser2025, staff001, /orgs/2/sites/site001
+g, staffUser2025, staff001, /orgs/2/sites/site002
+g, staffUser2025, staff001, /orgs/2/sites/site003
+g, staffUser2025, staff001, /orgs/2/sites/site004
+g, staffUser2025, staff001, /orgs/2/sites/site005
+
+g, staffUser2026, staff001, /orgs/2/sites/site001
+g, staffUser2026, staff001, /orgs/2/sites/site002
+g, staffUser2026, staff001, /orgs/2/sites/site003
+g, staffUser2026, staff001, /orgs/2/sites/site004
+g, staffUser2026, staff001, /orgs/2/sites/site005
+
+g, staffUser2027, staff001, /orgs/2/sites/site001
+g, staffUser2027, staff001, /orgs/2/sites/site002
+g, staffUser2027, staff001, /orgs/2/sites/site003
+g, staffUser2027, staff001, /orgs/2/sites/site004
+g, staffUser2027, staff001, /orgs/2/sites/site005
+
+g, staffUser2028, staff001, /orgs/2/sites/site001
+g, staffUser2028, staff001, /orgs/2/sites/site002
+g, staffUser2028, staff001, /orgs/2/sites/site003
+g, staffUser2028, staff001, /orgs/2/sites/site004
+g, staffUser2028, staff001, /orgs/2/sites/site005
+
+g, staffUser2029, staff001, /orgs/2/sites/site001
+g, staffUser2029, staff001, /orgs/2/sites/site002
+g, staffUser2029, staff001, /orgs/2/sites/site003
+g, staffUser2029, staff001, /orgs/2/sites/site004
+g, staffUser2029, staff001, /orgs/2/sites/site005
+
+g, staffUser2030, staff001, /orgs/2/sites/site001
+g, staffUser2030, staff001, /orgs/2/sites/site002
+g, staffUser2030, staff001, /orgs/2/sites/site003
+g, staffUser2030, staff001, /orgs/2/sites/site004
+g, staffUser2030, staff001, /orgs/2/sites/site005
+
+g, staffUser2031, staff001, /orgs/2/sites/site001
+g, staffUser2031, staff001, /orgs/2/sites/site002
+g, staffUser2031, staff001, /orgs/2/sites/site003
+g, staffUser2031, staff001, /orgs/2/sites/site004
+g, staffUser2031, staff001, /orgs/2/sites/site005
+
+g, staffUser2032, staff001, /orgs/2/sites/site001
+g, staffUser2032, staff001, /orgs/2/sites/site002
+g, staffUser2032, staff001, /orgs/2/sites/site003
+g, staffUser2032, staff001, /orgs/2/sites/site004
+g, staffUser2032, staff001, /orgs/2/sites/site005
+
+g, staffUser2033, staff001, /orgs/2/sites/site001
+g, staffUser2033, staff001, /orgs/2/sites/site002
+g, staffUser2033, staff001, /orgs/2/sites/site003
+g, staffUser2033, staff001, /orgs/2/sites/site004
+g, staffUser2033, staff001, /orgs/2/sites/site005
+
+g, staffUser2034, staff001, /orgs/2/sites/site001
+g, staffUser2034, staff001, /orgs/2/sites/site002
+g, staffUser2034, staff001, /orgs/2/sites/site003
+g, staffUser2034, staff001, /orgs/2/sites/site004
+g, staffUser2034, staff001, /orgs/2/sites/site005
+
+g, staffUser2035, staff001, /orgs/2/sites/site001
+g, staffUser2035, staff001, /orgs/2/sites/site002
+g, staffUser2035, staff001, /orgs/2/sites/site003
+g, staffUser2035, staff001, /orgs/2/sites/site004
+g, staffUser2035, staff001, /orgs/2/sites/site005
+
+g, staffUser2036, staff001, /orgs/2/sites/site001
+g, staffUser2036, staff001, /orgs/2/sites/site002
+g, staffUser2036, staff001, /orgs/2/sites/site003
+g, staffUser2036, staff001, /orgs/2/sites/site004
+g, staffUser2036, staff001, /orgs/2/sites/site005
+
+g, staffUser2037, staff001, /orgs/2/sites/site001
+g, staffUser2037, staff001, /orgs/2/sites/site002
+g, staffUser2037, staff001, /orgs/2/sites/site003
+g, staffUser2037, staff001, /orgs/2/sites/site004
+g, staffUser2037, staff001, /orgs/2/sites/site005
+
+g, staffUser2038, staff001, /orgs/2/sites/site001
+g, staffUser2038, staff001, /orgs/2/sites/site002
+g, staffUser2038, staff001, /orgs/2/sites/site003
+g, staffUser2038, staff001, /orgs/2/sites/site004
+g, staffUser2038, staff001, /orgs/2/sites/site005
+
+g, staffUser2039, staff001, /orgs/2/sites/site001
+g, staffUser2039, staff001, /orgs/2/sites/site002
+g, staffUser2039, staff001, /orgs/2/sites/site003
+g, staffUser2039, staff001, /orgs/2/sites/site004
+g, staffUser2039, staff001, /orgs/2/sites/site005
+
+g, staffUser2040, staff001, /orgs/2/sites/site001
+g, staffUser2040, staff001, /orgs/2/sites/site002
+g, staffUser2040, staff001, /orgs/2/sites/site003
+g, staffUser2040, staff001, /orgs/2/sites/site004
+g, staffUser2040, staff001, /orgs/2/sites/site005
+
+g, staffUser2041, staff001, /orgs/2/sites/site001
+g, staffUser2041, staff001, /orgs/2/sites/site002
+g, staffUser2041, staff001, /orgs/2/sites/site003
+g, staffUser2041, staff001, /orgs/2/sites/site004
+g, staffUser2041, staff001, /orgs/2/sites/site005
+
+g, staffUser2042, staff001, /orgs/2/sites/site001
+g, staffUser2042, staff001, /orgs/2/sites/site002
+g, staffUser2042, staff001, /orgs/2/sites/site003
+g, staffUser2042, staff001, /orgs/2/sites/site004
+g, staffUser2042, staff001, /orgs/2/sites/site005
+
+g, staffUser2043, staff001, /orgs/2/sites/site001
+g, staffUser2043, staff001, /orgs/2/sites/site002
+g, staffUser2043, staff001, /orgs/2/sites/site003
+g, staffUser2043, staff001, /orgs/2/sites/site004
+g, staffUser2043, staff001, /orgs/2/sites/site005
+
+g, staffUser2044, staff001, /orgs/2/sites/site001
+g, staffUser2044, staff001, /orgs/2/sites/site002
+g, staffUser2044, staff001, /orgs/2/sites/site003
+g, staffUser2044, staff001, /orgs/2/sites/site004
+g, staffUser2044, staff001, /orgs/2/sites/site005
+
+g, staffUser2045, staff001, /orgs/2/sites/site001
+g, staffUser2045, staff001, /orgs/2/sites/site002
+g, staffUser2045, staff001, /orgs/2/sites/site003
+g, staffUser2045, staff001, /orgs/2/sites/site004
+g, staffUser2045, staff001, /orgs/2/sites/site005
+
+g, staffUser2046, staff001, /orgs/2/sites/site001
+g, staffUser2046, staff001, /orgs/2/sites/site002
+g, staffUser2046, staff001, /orgs/2/sites/site003
+g, staffUser2046, staff001, /orgs/2/sites/site004
+g, staffUser2046, staff001, /orgs/2/sites/site005
+
+g, staffUser2047, staff001, /orgs/2/sites/site001
+g, staffUser2047, staff001, /orgs/2/sites/site002
+g, staffUser2047, staff001, /orgs/2/sites/site003
+g, staffUser2047, staff001, /orgs/2/sites/site004
+g, staffUser2047, staff001, /orgs/2/sites/site005
+
+g, staffUser2048, staff001, /orgs/2/sites/site001
+g, staffUser2048, staff001, /orgs/2/sites/site002
+g, staffUser2048, staff001, /orgs/2/sites/site003
+g, staffUser2048, staff001, /orgs/2/sites/site004
+g, staffUser2048, staff001, /orgs/2/sites/site005
+
+g, staffUser2049, staff001, /orgs/2/sites/site001
+g, staffUser2049, staff001, /orgs/2/sites/site002
+g, staffUser2049, staff001, /orgs/2/sites/site003
+g, staffUser2049, staff001, /orgs/2/sites/site004
+g, staffUser2049, staff001, /orgs/2/sites/site005
+
+g, staffUser2050, staff001, /orgs/2/sites/site001
+g, staffUser2050, staff001, /orgs/2/sites/site002
+g, staffUser2050, staff001, /orgs/2/sites/site003
+g, staffUser2050, staff001, /orgs/2/sites/site004
+g, staffUser2050, staff001, /orgs/2/sites/site005
+
+# Group - manager001, / org2
+g, managerUser1001, manager001, /orgs/2/sites/site001
+g, managerUser1001, manager001, /orgs/2/sites/site002
+g, managerUser1001, manager001, /orgs/2/sites/site003
+g, managerUser1001, manager001, /orgs/2/sites/site004
+g, managerUser1001, manager001, /orgs/2/sites/site005
+
+g, managerUser1001, manager001, /orgs/2/sites/site001
+g, managerUser1001, manager001, /orgs/2/sites/site002
+g, managerUser1001, manager001, /orgs/2/sites/site003
+g, managerUser1001, manager001, /orgs/2/sites/site004
+g, managerUser1001, manager001, /orgs/2/sites/site005
+
+g, managerUser1003, manager001, /orgs/2/sites/site001
+g, managerUser1003, manager001, /orgs/2/sites/site002
+g, managerUser1003, manager001, /orgs/2/sites/site003
+g, managerUser1003, manager001, /orgs/2/sites/site004
+g, managerUser1003, manager001, /orgs/2/sites/site005
+
+g, managerUser1004, manager001, /orgs/2/sites/site001
+g, managerUser1004, manager001, /orgs/2/sites/site002
+g, managerUser1004, manager001, /orgs/2/sites/site003
+g, managerUser1004, manager001, /orgs/2/sites/site004
+g, managerUser1004, manager001, /orgs/2/sites/site005
+
+g, managerUser1005, manager001, /orgs/2/sites/site001
+g, managerUser1005, manager001, /orgs/2/sites/site002
+g, managerUser1005, manager001, /orgs/2/sites/site003
+g, managerUser1005, manager001, /orgs/2/sites/site004
+g, managerUser1005, manager001, /orgs/2/sites/site005
+
+g, managerUser1006, manager001, /orgs/2/sites/site001
+g, managerUser1006, manager001, /orgs/2/sites/site002
+g, managerUser1006, manager001, /orgs/2/sites/site003
+g, managerUser1006, manager001, /orgs/2/sites/site004
+g, managerUser1006, manager001, /orgs/2/sites/site005
+
+g, managerUser1007, manager001, /orgs/2/sites/site001
+g, managerUser1007, manager001, /orgs/2/sites/site002
+g, managerUser1007, manager001, /orgs/2/sites/site003
+g, managerUser1007, manager001, /orgs/2/sites/site004
+g, managerUser1007, manager001, /orgs/2/sites/site005
+
+g, managerUser1008, manager001, /orgs/2/sites/site001
+g, managerUser1008, manager001, /orgs/2/sites/site002
+g, managerUser1008, manager001, /orgs/2/sites/site003
+g, managerUser1008, manager001, /orgs/2/sites/site004
+g, managerUser1008, manager001, /orgs/2/sites/site005
+
+g, managerUser1009, manager001, /orgs/2/sites/site001
+g, managerUser1009, manager001, /orgs/2/sites/site002
+g, managerUser1009, manager001, /orgs/2/sites/site003
+g, managerUser1009, manager001, /orgs/2/sites/site004
+g, managerUser1009, manager001, /orgs/2/sites/site005
+
+g, managerUser1010, manager001, /orgs/2/sites/site001
+g, managerUser1010, manager001, /orgs/2/sites/site002
+g, managerUser1010, manager001, /orgs/2/sites/site003
+g, managerUser1010, manager001, /orgs/2/sites/site004
+g, managerUser1010, manager001, /orgs/2/sites/site005
+
+g, managerUser1011, manager001, /orgs/2/sites/site001
+g, managerUser1011, manager001, /orgs/2/sites/site002
+g, managerUser1011, manager001, /orgs/2/sites/site003
+g, managerUser1011, manager001, /orgs/2/sites/site004
+g, managerUser1011, manager001, /orgs/2/sites/site005
+
+g, managerUser1012, manager001, /orgs/2/sites/site001
+g, managerUser1012, manager001, /orgs/2/sites/site002
+g, managerUser1012, manager001, /orgs/2/sites/site003
+g, managerUser1012, manager001, /orgs/2/sites/site004
+g, managerUser1012, manager001, /orgs/2/sites/site005
+
+g, managerUser1013, manager001, /orgs/2/sites/site001
+g, managerUser1013, manager001, /orgs/2/sites/site002
+g, managerUser1013, manager001, /orgs/2/sites/site003
+g, managerUser1013, manager001, /orgs/2/sites/site004
+g, managerUser1013, manager001, /orgs/2/sites/site005
+
+g, managerUser1014, manager001, /orgs/2/sites/site001
+g, managerUser1014, manager001, /orgs/2/sites/site002
+g, managerUser1014, manager001, /orgs/2/sites/site003
+g, managerUser1014, manager001, /orgs/2/sites/site004
+g, managerUser1014, manager001, /orgs/2/sites/site005
+
+g, managerUser1015, manager001, /orgs/2/sites/site001
+g, managerUser1015, manager001, /orgs/2/sites/site002
+g, managerUser1015, manager001, /orgs/2/sites/site003
+g, managerUser1015, manager001, /orgs/2/sites/site004
+g, managerUser1015, manager001, /orgs/2/sites/site005
+
+g, managerUser1016, manager001, /orgs/2/sites/site001
+g, managerUser1016, manager001, /orgs/2/sites/site002
+g, managerUser1016, manager001, /orgs/2/sites/site003
+g, managerUser1016, manager001, /orgs/2/sites/site004
+g, managerUser1016, manager001, /orgs/2/sites/site005
+
+g, managerUser1017, manager001, /orgs/2/sites/site001
+g, managerUser1017, manager001, /orgs/2/sites/site002
+g, managerUser1017, manager001, /orgs/2/sites/site003
+g, managerUser1017, manager001, /orgs/2/sites/site004
+g, managerUser1017, manager001, /orgs/2/sites/site005
+
+g, managerUser1018, manager001, /orgs/2/sites/site001
+g, managerUser1018, manager001, /orgs/2/sites/site002
+g, managerUser1018, manager001, /orgs/2/sites/site003
+g, managerUser1018, manager001, /orgs/2/sites/site004
+g, managerUser1018, manager001, /orgs/2/sites/site005
+
+g, managerUser1019, manager001, /orgs/2/sites/site001
+g, managerUser1019, manager001, /orgs/2/sites/site002
+g, managerUser1019, manager001, /orgs/2/sites/site003
+g, managerUser1019, manager001, /orgs/2/sites/site004
+g, managerUser1019, manager001, /orgs/2/sites/site005
+
+g, managerUser1020, manager001, /orgs/2/sites/site001
+g, managerUser1020, manager001, /orgs/2/sites/site002
+g, managerUser1020, manager001, /orgs/2/sites/site003
+g, managerUser1020, manager001, /orgs/2/sites/site004
+g, managerUser1020, manager001, /orgs/2/sites/site005
+
+g, managerUser1021, manager001, /orgs/2/sites/site001
+g, managerUser1021, manager001, /orgs/2/sites/site002
+g, managerUser1021, manager001, /orgs/2/sites/site003
+g, managerUser1021, manager001, /orgs/2/sites/site004
+g, managerUser1021, manager001, /orgs/2/sites/site005
+
+g, managerUser1022, manager001, /orgs/2/sites/site001
+g, managerUser1022, manager001, /orgs/2/sites/site002
+g, managerUser1022, manager001, /orgs/2/sites/site003
+g, managerUser1022, manager001, /orgs/2/sites/site004
+g, managerUser1022, manager001, /orgs/2/sites/site005
+
+g, managerUser1023, manager001, /orgs/2/sites/site001
+g, managerUser1023, manager001, /orgs/2/sites/site002
+g, managerUser1023, manager001, /orgs/2/sites/site003
+g, managerUser1023, manager001, /orgs/2/sites/site004
+g, managerUser1023, manager001, /orgs/2/sites/site005
+
+g, managerUser1024, manager001, /orgs/2/sites/site001
+g, managerUser1024, manager001, /orgs/2/sites/site002
+g, managerUser1024, manager001, /orgs/2/sites/site003
+g, managerUser1024, manager001, /orgs/2/sites/site004
+g, managerUser1024, manager001, /orgs/2/sites/site005
+
+g, managerUser1025, manager001, /orgs/2/sites/site001
+g, managerUser1025, manager001, /orgs/2/sites/site002
+g, managerUser1025, manager001, /orgs/2/sites/site003
+g, managerUser1025, manager001, /orgs/2/sites/site004
+g, managerUser1025, manager001, /orgs/2/sites/site005
+
+g, managerUser1026, manager001, /orgs/2/sites/site001
+g, managerUser1026, manager001, /orgs/2/sites/site002
+g, managerUser1026, manager001, /orgs/2/sites/site003
+g, managerUser1026, manager001, /orgs/2/sites/site004
+g, managerUser1026, manager001, /orgs/2/sites/site005
+
+g, managerUser1027, manager001, /orgs/2/sites/site001
+g, managerUser1027, manager001, /orgs/2/sites/site002
+g, managerUser1027, manager001, /orgs/2/sites/site003
+g, managerUser1027, manager001, /orgs/2/sites/site004
+g, managerUser1027, manager001, /orgs/2/sites/site005
+
+g, managerUser1028, manager001, /orgs/2/sites/site001
+g, managerUser1028, manager001, /orgs/2/sites/site002
+g, managerUser1028, manager001, /orgs/2/sites/site003
+g, managerUser1028, manager001, /orgs/2/sites/site004
+g, managerUser1028, manager001, /orgs/2/sites/site005
+
+g, managerUser1029, manager001, /orgs/2/sites/site001
+g, managerUser1029, manager001, /orgs/2/sites/site002
+g, managerUser1029, manager001, /orgs/2/sites/site003
+g, managerUser1029, manager001, /orgs/2/sites/site004
+g, managerUser1029, manager001, /orgs/2/sites/site005
+
+g, managerUser1030, manager001, /orgs/2/sites/site001
+g, managerUser1030, manager001, /orgs/2/sites/site002
+g, managerUser1030, manager001, /orgs/2/sites/site003
+g, managerUser1030, manager001, /orgs/2/sites/site004
+g, managerUser1030, manager001, /orgs/2/sites/site005
+
+g, managerUser1031, manager001, /orgs/2/sites/site001
+g, managerUser1031, manager001, /orgs/2/sites/site002
+g, managerUser1031, manager001, /orgs/2/sites/site003
+g, managerUser1031, manager001, /orgs/2/sites/site004
+g, managerUser1031, manager001, /orgs/2/sites/site005
+
+g, managerUser1032, manager001, /orgs/2/sites/site001
+g, managerUser1032, manager001, /orgs/2/sites/site002
+g, managerUser1032, manager001, /orgs/2/sites/site003
+g, managerUser1032, manager001, /orgs/2/sites/site004
+g, managerUser1032, manager001, /orgs/2/sites/site005
+
+g, managerUser1033, manager001, /orgs/2/sites/site001
+g, managerUser1033, manager001, /orgs/2/sites/site002
+g, managerUser1033, manager001, /orgs/2/sites/site003
+g, managerUser1033, manager001, /orgs/2/sites/site004
+g, managerUser1033, manager001, /orgs/2/sites/site005
+
+g, managerUser1034, manager001, /orgs/2/sites/site001
+g, managerUser1034, manager001, /orgs/2/sites/site002
+g, managerUser1034, manager001, /orgs/2/sites/site003
+g, managerUser1034, manager001, /orgs/2/sites/site004
+g, managerUser1034, manager001, /orgs/2/sites/site005
+
+g, managerUser1035, manager001, /orgs/2/sites/site001
+g, managerUser1035, manager001, /orgs/2/sites/site002
+g, managerUser1035, manager001, /orgs/2/sites/site003
+g, managerUser1035, manager001, /orgs/2/sites/site004
+g, managerUser1035, manager001, /orgs/2/sites/site005
+
+g, managerUser1036, manager001, /orgs/2/sites/site001
+g, managerUser1036, manager001, /orgs/2/sites/site002
+g, managerUser1036, manager001, /orgs/2/sites/site003
+g, managerUser1036, manager001, /orgs/2/sites/site004
+g, managerUser1036, manager001, /orgs/2/sites/site005
+
+g, managerUser1037, manager001, /orgs/2/sites/site001
+g, managerUser1037, manager001, /orgs/2/sites/site002
+g, managerUser1037, manager001, /orgs/2/sites/site003
+g, managerUser1037, manager001, /orgs/2/sites/site004
+g, managerUser1037, manager001, /orgs/2/sites/site005
+
+g, managerUser1038, manager001, /orgs/2/sites/site001
+g, managerUser1038, manager001, /orgs/2/sites/site002
+g, managerUser1038, manager001, /orgs/2/sites/site003
+g, managerUser1038, manager001, /orgs/2/sites/site004
+g, managerUser1038, manager001, /orgs/2/sites/site005
+
+g, managerUser1039, manager001, /orgs/2/sites/site001
+g, managerUser1039, manager001, /orgs/2/sites/site002
+g, managerUser1039, manager001, /orgs/2/sites/site003
+g, managerUser1039, manager001, /orgs/2/sites/site004
+g, managerUser1039, manager001, /orgs/2/sites/site005
+
+g, managerUser1040, manager001, /orgs/2/sites/site001
+g, managerUser1040, manager001, /orgs/2/sites/site002
+g, managerUser1040, manager001, /orgs/2/sites/site003
+g, managerUser1040, manager001, /orgs/2/sites/site004
+g, managerUser1040, manager001, /orgs/2/sites/site005
+
+g, managerUser1041, manager001, /orgs/2/sites/site001
+g, managerUser1041, manager001, /orgs/2/sites/site002
+g, managerUser1041, manager001, /orgs/2/sites/site003
+g, managerUser1041, manager001, /orgs/2/sites/site004
+g, managerUser1041, manager001, /orgs/2/sites/site005
+
+g, managerUser1042, manager001, /orgs/2/sites/site001
+g, managerUser1042, manager001, /orgs/2/sites/site002
+g, managerUser1042, manager001, /orgs/2/sites/site003
+g, managerUser1042, manager001, /orgs/2/sites/site004
+g, managerUser1042, manager001, /orgs/2/sites/site005
+
+g, managerUser1043, manager001, /orgs/2/sites/site001
+g, managerUser1043, manager001, /orgs/2/sites/site002
+g, managerUser1043, manager001, /orgs/2/sites/site003
+g, managerUser1043, manager001, /orgs/2/sites/site004
+g, managerUser1043, manager001, /orgs/2/sites/site005
+
+g, managerUser1044, manager001, /orgs/2/sites/site001
+g, managerUser1044, manager001, /orgs/2/sites/site002
+g, managerUser1044, manager001, /orgs/2/sites/site003
+g, managerUser1044, manager001, /orgs/2/sites/site004
+g, managerUser1044, manager001, /orgs/2/sites/site005
+
+g, managerUser1045, manager001, /orgs/2/sites/site001
+g, managerUser1045, manager001, /orgs/2/sites/site002
+g, managerUser1045, manager001, /orgs/2/sites/site003
+g, managerUser1045, manager001, /orgs/2/sites/site004
+g, managerUser1045, manager001, /orgs/2/sites/site005
+
+g, managerUser1046, manager001, /orgs/2/sites/site001
+g, managerUser1046, manager001, /orgs/2/sites/site002
+g, managerUser1046, manager001, /orgs/2/sites/site003
+g, managerUser1046, manager001, /orgs/2/sites/site004
+g, managerUser1046, manager001, /orgs/2/sites/site005
+
+g, managerUser1047, manager001, /orgs/2/sites/site001
+g, managerUser1047, manager001, /orgs/2/sites/site002
+g, managerUser1047, manager001, /orgs/2/sites/site003
+g, managerUser1047, manager001, /orgs/2/sites/site004
+g, managerUser1047, manager001, /orgs/2/sites/site005
+
+g, managerUser1048, manager001, /orgs/2/sites/site001
+g, managerUser1048, manager001, /orgs/2/sites/site002
+g, managerUser1048, manager001, /orgs/2/sites/site003
+g, managerUser1048, manager001, /orgs/2/sites/site004
+g, managerUser1048, manager001, /orgs/2/sites/site005
+
+g, managerUser1049, manager001, /orgs/2/sites/site001
+g, managerUser1049, manager001, /orgs/2/sites/site002
+g, managerUser1049, manager001, /orgs/2/sites/site003
+g, managerUser1049, manager001, /orgs/2/sites/site004
+g, managerUser1049, manager001, /orgs/2/sites/site005
+
+g, managerUser1050, manager001, /orgs/2/sites/site001
+g, managerUser1050, manager001, /orgs/2/sites/site002
+g, managerUser1050, manager001, /orgs/2/sites/site003
+g, managerUser1050, manager001, /orgs/2/sites/site004
+g, managerUser1050, manager001, /orgs/2/sites/site005
+
+# Group - manager001, / org2
+g, managerUser2001, manager001, /orgs/2/sites/site001
+g, managerUser2001, manager001, /orgs/2/sites/site002
+g, managerUser2001, manager001, /orgs/2/sites/site003
+g, managerUser2001, manager001, /orgs/2/sites/site004
+g, managerUser2001, manager001, /orgs/2/sites/site005
+
+g, managerUser2001, manager001, /orgs/2/sites/site001
+g, managerUser2001, manager001, /orgs/2/sites/site002
+g, managerUser2001, manager001, /orgs/2/sites/site003
+g, managerUser2001, manager001, /orgs/2/sites/site004
+g, managerUser2001, manager001, /orgs/2/sites/site005
+
+g, managerUser2003, manager001, /orgs/2/sites/site001
+g, managerUser2003, manager001, /orgs/2/sites/site002
+g, managerUser2003, manager001, /orgs/2/sites/site003
+g, managerUser2003, manager001, /orgs/2/sites/site004
+g, managerUser2003, manager001, /orgs/2/sites/site005
+
+g, managerUser2004, manager001, /orgs/2/sites/site001
+g, managerUser2004, manager001, /orgs/2/sites/site002
+g, managerUser2004, manager001, /orgs/2/sites/site003
+g, managerUser2004, manager001, /orgs/2/sites/site004
+g, managerUser2004, manager001, /orgs/2/sites/site005
+
+g, managerUser2005, manager001, /orgs/2/sites/site001
+g, managerUser2005, manager001, /orgs/2/sites/site002
+g, managerUser2005, manager001, /orgs/2/sites/site003
+g, managerUser2005, manager001, /orgs/2/sites/site004
+g, managerUser2005, manager001, /orgs/2/sites/site005
+
+g, managerUser2006, manager001, /orgs/2/sites/site001
+g, managerUser2006, manager001, /orgs/2/sites/site002
+g, managerUser2006, manager001, /orgs/2/sites/site003
+g, managerUser2006, manager001, /orgs/2/sites/site004
+g, managerUser2006, manager001, /orgs/2/sites/site005
+
+g, managerUser2007, manager001, /orgs/2/sites/site001
+g, managerUser2007, manager001, /orgs/2/sites/site002
+g, managerUser2007, manager001, /orgs/2/sites/site003
+g, managerUser2007, manager001, /orgs/2/sites/site004
+g, managerUser2007, manager001, /orgs/2/sites/site005
+
+g, managerUser2008, manager001, /orgs/2/sites/site001
+g, managerUser2008, manager001, /orgs/2/sites/site002
+g, managerUser2008, manager001, /orgs/2/sites/site003
+g, managerUser2008, manager001, /orgs/2/sites/site004
+g, managerUser2008, manager001, /orgs/2/sites/site005
+
+g, managerUser2009, manager001, /orgs/2/sites/site001
+g, managerUser2009, manager001, /orgs/2/sites/site002
+g, managerUser2009, manager001, /orgs/2/sites/site003
+g, managerUser2009, manager001, /orgs/2/sites/site004
+g, managerUser2009, manager001, /orgs/2/sites/site005
+
+g, managerUser2010, manager001, /orgs/2/sites/site001
+g, managerUser2010, manager001, /orgs/2/sites/site002
+g, managerUser2010, manager001, /orgs/2/sites/site003
+g, managerUser2010, manager001, /orgs/2/sites/site004
+g, managerUser2010, manager001, /orgs/2/sites/site005
+
+g, managerUser2011, manager001, /orgs/2/sites/site001
+g, managerUser2011, manager001, /orgs/2/sites/site002
+g, managerUser2011, manager001, /orgs/2/sites/site003
+g, managerUser2011, manager001, /orgs/2/sites/site004
+g, managerUser2011, manager001, /orgs/2/sites/site005
+
+g, managerUser2012, manager001, /orgs/2/sites/site001
+g, managerUser2012, manager001, /orgs/2/sites/site002
+g, managerUser2012, manager001, /orgs/2/sites/site003
+g, managerUser2012, manager001, /orgs/2/sites/site004
+g, managerUser2012, manager001, /orgs/2/sites/site005
+
+g, managerUser2013, manager001, /orgs/2/sites/site001
+g, managerUser2013, manager001, /orgs/2/sites/site002
+g, managerUser2013, manager001, /orgs/2/sites/site003
+g, managerUser2013, manager001, /orgs/2/sites/site004
+g, managerUser2013, manager001, /orgs/2/sites/site005
+
+g, managerUser2014, manager001, /orgs/2/sites/site001
+g, managerUser2014, manager001, /orgs/2/sites/site002
+g, managerUser2014, manager001, /orgs/2/sites/site003
+g, managerUser2014, manager001, /orgs/2/sites/site004
+g, managerUser2014, manager001, /orgs/2/sites/site005
+
+g, managerUser2015, manager001, /orgs/2/sites/site001
+g, managerUser2015, manager001, /orgs/2/sites/site002
+g, managerUser2015, manager001, /orgs/2/sites/site003
+g, managerUser2015, manager001, /orgs/2/sites/site004
+g, managerUser2015, manager001, /orgs/2/sites/site005
+
+g, managerUser2016, manager001, /orgs/2/sites/site001
+g, managerUser2016, manager001, /orgs/2/sites/site002
+g, managerUser2016, manager001, /orgs/2/sites/site003
+g, managerUser2016, manager001, /orgs/2/sites/site004
+g, managerUser2016, manager001, /orgs/2/sites/site005
+
+g, managerUser2017, manager001, /orgs/2/sites/site001
+g, managerUser2017, manager001, /orgs/2/sites/site002
+g, managerUser2017, manager001, /orgs/2/sites/site003
+g, managerUser2017, manager001, /orgs/2/sites/site004
+g, managerUser2017, manager001, /orgs/2/sites/site005
+
+g, managerUser2018, manager001, /orgs/2/sites/site001
+g, managerUser2018, manager001, /orgs/2/sites/site002
+g, managerUser2018, manager001, /orgs/2/sites/site003
+g, managerUser2018, manager001, /orgs/2/sites/site004
+g, managerUser2018, manager001, /orgs/2/sites/site005
+
+g, managerUser2019, manager001, /orgs/2/sites/site001
+g, managerUser2019, manager001, /orgs/2/sites/site002
+g, managerUser2019, manager001, /orgs/2/sites/site003
+g, managerUser2019, manager001, /orgs/2/sites/site004
+g, managerUser2019, manager001, /orgs/2/sites/site005
+
+g, managerUser2020, manager001, /orgs/2/sites/site001
+g, managerUser2020, manager001, /orgs/2/sites/site002
+g, managerUser2020, manager001, /orgs/2/sites/site003
+g, managerUser2020, manager001, /orgs/2/sites/site004
+g, managerUser2020, manager001, /orgs/2/sites/site005
+
+g, managerUser2021, manager001, /orgs/2/sites/site001
+g, managerUser2021, manager001, /orgs/2/sites/site002
+g, managerUser2021, manager001, /orgs/2/sites/site003
+g, managerUser2021, manager001, /orgs/2/sites/site004
+g, managerUser2021, manager001, /orgs/2/sites/site005
+
+g, managerUser2022, manager001, /orgs/2/sites/site001
+g, managerUser2022, manager001, /orgs/2/sites/site002
+g, managerUser2022, manager001, /orgs/2/sites/site003
+g, managerUser2022, manager001, /orgs/2/sites/site004
+g, managerUser2022, manager001, /orgs/2/sites/site005
+
+g, managerUser2023, manager001, /orgs/2/sites/site001
+g, managerUser2023, manager001, /orgs/2/sites/site002
+g, managerUser2023, manager001, /orgs/2/sites/site003
+g, managerUser2023, manager001, /orgs/2/sites/site004
+g, managerUser2023, manager001, /orgs/2/sites/site005
+
+g, managerUser2024, manager001, /orgs/2/sites/site001
+g, managerUser2024, manager001, /orgs/2/sites/site002
+g, managerUser2024, manager001, /orgs/2/sites/site003
+g, managerUser2024, manager001, /orgs/2/sites/site004
+g, managerUser2024, manager001, /orgs/2/sites/site005
+
+g, managerUser2025, manager001, /orgs/2/sites/site001
+g, managerUser2025, manager001, /orgs/2/sites/site002
+g, managerUser2025, manager001, /orgs/2/sites/site003
+g, managerUser2025, manager001, /orgs/2/sites/site004
+g, managerUser2025, manager001, /orgs/2/sites/site005
+
+g, managerUser2026, manager001, /orgs/2/sites/site001
+g, managerUser2026, manager001, /orgs/2/sites/site002
+g, managerUser2026, manager001, /orgs/2/sites/site003
+g, managerUser2026, manager001, /orgs/2/sites/site004
+g, managerUser2026, manager001, /orgs/2/sites/site005
+
+g, managerUser2027, manager001, /orgs/2/sites/site001
+g, managerUser2027, manager001, /orgs/2/sites/site002
+g, managerUser2027, manager001, /orgs/2/sites/site003
+g, managerUser2027, manager001, /orgs/2/sites/site004
+g, managerUser2027, manager001, /orgs/2/sites/site005
+
+g, managerUser2028, manager001, /orgs/2/sites/site001
+g, managerUser2028, manager001, /orgs/2/sites/site002
+g, managerUser2028, manager001, /orgs/2/sites/site003
+g, managerUser2028, manager001, /orgs/2/sites/site004
+g, managerUser2028, manager001, /orgs/2/sites/site005
+
+g, managerUser2029, manager001, /orgs/2/sites/site001
+g, managerUser2029, manager001, /orgs/2/sites/site002
+g, managerUser2029, manager001, /orgs/2/sites/site003
+g, managerUser2029, manager001, /orgs/2/sites/site004
+g, managerUser2029, manager001, /orgs/2/sites/site005
+
+g, managerUser2030, manager001, /orgs/2/sites/site001
+g, managerUser2030, manager001, /orgs/2/sites/site002
+g, managerUser2030, manager001, /orgs/2/sites/site003
+g, managerUser2030, manager001, /orgs/2/sites/site004
+g, managerUser2030, manager001, /orgs/2/sites/site005
+
+g, managerUser2031, manager001, /orgs/2/sites/site001
+g, managerUser2031, manager001, /orgs/2/sites/site002
+g, managerUser2031, manager001, /orgs/2/sites/site003
+g, managerUser2031, manager001, /orgs/2/sites/site004
+g, managerUser2031, manager001, /orgs/2/sites/site005
+
+g, managerUser2032, manager001, /orgs/2/sites/site001
+g, managerUser2032, manager001, /orgs/2/sites/site002
+g, managerUser2032, manager001, /orgs/2/sites/site003
+g, managerUser2032, manager001, /orgs/2/sites/site004
+g, managerUser2032, manager001, /orgs/2/sites/site005
+
+g, managerUser2033, manager001, /orgs/2/sites/site001
+g, managerUser2033, manager001, /orgs/2/sites/site002
+g, managerUser2033, manager001, /orgs/2/sites/site003
+g, managerUser2033, manager001, /orgs/2/sites/site004
+g, managerUser2033, manager001, /orgs/2/sites/site005
+
+g, managerUser2034, manager001, /orgs/2/sites/site001
+g, managerUser2034, manager001, /orgs/2/sites/site002
+g, managerUser2034, manager001, /orgs/2/sites/site003
+g, managerUser2034, manager001, /orgs/2/sites/site004
+g, managerUser2034, manager001, /orgs/2/sites/site005
+
+g, managerUser2035, manager001, /orgs/2/sites/site001
+g, managerUser2035, manager001, /orgs/2/sites/site002
+g, managerUser2035, manager001, /orgs/2/sites/site003
+g, managerUser2035, manager001, /orgs/2/sites/site004
+g, managerUser2035, manager001, /orgs/2/sites/site005
+
+g, managerUser2036, manager001, /orgs/2/sites/site001
+g, managerUser2036, manager001, /orgs/2/sites/site002
+g, managerUser2036, manager001, /orgs/2/sites/site003
+g, managerUser2036, manager001, /orgs/2/sites/site004
+g, managerUser2036, manager001, /orgs/2/sites/site005
+
+g, managerUser2037, manager001, /orgs/2/sites/site001
+g, managerUser2037, manager001, /orgs/2/sites/site002
+g, managerUser2037, manager001, /orgs/2/sites/site003
+g, managerUser2037, manager001, /orgs/2/sites/site004
+g, managerUser2037, manager001, /orgs/2/sites/site005
+
+g, managerUser2038, manager001, /orgs/2/sites/site001
+g, managerUser2038, manager001, /orgs/2/sites/site002
+g, managerUser2038, manager001, /orgs/2/sites/site003
+g, managerUser2038, manager001, /orgs/2/sites/site004
+g, managerUser2038, manager001, /orgs/2/sites/site005
+
+g, managerUser2039, manager001, /orgs/2/sites/site001
+g, managerUser2039, manager001, /orgs/2/sites/site002
+g, managerUser2039, manager001, /orgs/2/sites/site003
+g, managerUser2039, manager001, /orgs/2/sites/site004
+g, managerUser2039, manager001, /orgs/2/sites/site005
+
+g, managerUser2040, manager001, /orgs/2/sites/site001
+g, managerUser2040, manager001, /orgs/2/sites/site002
+g, managerUser2040, manager001, /orgs/2/sites/site003
+g, managerUser2040, manager001, /orgs/2/sites/site004
+g, managerUser2040, manager001, /orgs/2/sites/site005
+
+g, managerUser2041, manager001, /orgs/2/sites/site001
+g, managerUser2041, manager001, /orgs/2/sites/site002
+g, managerUser2041, manager001, /orgs/2/sites/site003
+g, managerUser2041, manager001, /orgs/2/sites/site004
+g, managerUser2041, manager001, /orgs/2/sites/site005
+
+g, managerUser2042, manager001, /orgs/2/sites/site001
+g, managerUser2042, manager001, /orgs/2/sites/site002
+g, managerUser2042, manager001, /orgs/2/sites/site003
+g, managerUser2042, manager001, /orgs/2/sites/site004
+g, managerUser2042, manager001, /orgs/2/sites/site005
+
+g, managerUser2043, manager001, /orgs/2/sites/site001
+g, managerUser2043, manager001, /orgs/2/sites/site002
+g, managerUser2043, manager001, /orgs/2/sites/site003
+g, managerUser2043, manager001, /orgs/2/sites/site004
+g, managerUser2043, manager001, /orgs/2/sites/site005
+
+g, managerUser2044, manager001, /orgs/2/sites/site001
+g, managerUser2044, manager001, /orgs/2/sites/site002
+g, managerUser2044, manager001, /orgs/2/sites/site003
+g, managerUser2044, manager001, /orgs/2/sites/site004
+g, managerUser2044, manager001, /orgs/2/sites/site005
+
+g, managerUser2045, manager001, /orgs/2/sites/site001
+g, managerUser2045, manager001, /orgs/2/sites/site002
+g, managerUser2045, manager001, /orgs/2/sites/site003
+g, managerUser2045, manager001, /orgs/2/sites/site004
+g, managerUser2045, manager001, /orgs/2/sites/site005
+
+g, managerUser2046, manager001, /orgs/2/sites/site001
+g, managerUser2046, manager001, /orgs/2/sites/site002
+g, managerUser2046, manager001, /orgs/2/sites/site003
+g, managerUser2046, manager001, /orgs/2/sites/site004
+g, managerUser2046, manager001, /orgs/2/sites/site005
+
+g, managerUser2047, manager001, /orgs/2/sites/site001
+g, managerUser2047, manager001, /orgs/2/sites/site002
+g, managerUser2047, manager001, /orgs/2/sites/site003
+g, managerUser2047, manager001, /orgs/2/sites/site004
+g, managerUser2047, manager001, /orgs/2/sites/site005
+
+g, managerUser2048, manager001, /orgs/2/sites/site001
+g, managerUser2048, manager001, /orgs/2/sites/site002
+g, managerUser2048, manager001, /orgs/2/sites/site003
+g, managerUser2048, manager001, /orgs/2/sites/site004
+g, managerUser2048, manager001, /orgs/2/sites/site005
+
+g, managerUser2049, manager001, /orgs/2/sites/site001
+g, managerUser2049, manager001, /orgs/2/sites/site002
+g, managerUser2049, manager001, /orgs/2/sites/site003
+g, managerUser2049, manager001, /orgs/2/sites/site004
+g, managerUser2049, manager001, /orgs/2/sites/site005
+
+g, managerUser2050, manager001, /orgs/2/sites/site001
+g, managerUser2050, manager001, /orgs/2/sites/site002
+g, managerUser2050, manager001, /orgs/2/sites/site003
+g, managerUser2050, manager001, /orgs/2/sites/site004
+g, managerUser2050, manager001, /orgs/2/sites/site005
+
+# Group - customer001, / org2
+g, customerUser1001, customer001, /orgs/2/sites/site001
+g, customerUser1001, customer001, /orgs/2/sites/site002
+g, customerUser1001, customer001, /orgs/2/sites/site003
+g, customerUser1001, customer001, /orgs/2/sites/site004
+g, customerUser1001, customer001, /orgs/2/sites/site005
+
+g, customerUser1001, customer001, /orgs/2/sites/site001
+g, customerUser1001, customer001, /orgs/2/sites/site002
+g, customerUser1001, customer001, /orgs/2/sites/site003
+g, customerUser1001, customer001, /orgs/2/sites/site004
+g, customerUser1001, customer001, /orgs/2/sites/site005
+
+g, customerUser1003, customer001, /orgs/2/sites/site001
+g, customerUser1003, customer001, /orgs/2/sites/site002
+g, customerUser1003, customer001, /orgs/2/sites/site003
+g, customerUser1003, customer001, /orgs/2/sites/site004
+g, customerUser1003, customer001, /orgs/2/sites/site005
+
+g, customerUser1004, customer001, /orgs/2/sites/site001
+g, customerUser1004, customer001, /orgs/2/sites/site002
+g, customerUser1004, customer001, /orgs/2/sites/site003
+g, customerUser1004, customer001, /orgs/2/sites/site004
+g, customerUser1004, customer001, /orgs/2/sites/site005
+
+g, customerUser1005, customer001, /orgs/2/sites/site001
+g, customerUser1005, customer001, /orgs/2/sites/site002
+g, customerUser1005, customer001, /orgs/2/sites/site003
+g, customerUser1005, customer001, /orgs/2/sites/site004
+g, customerUser1005, customer001, /orgs/2/sites/site005
+
+g, customerUser1006, customer001, /orgs/2/sites/site001
+g, customerUser1006, customer001, /orgs/2/sites/site002
+g, customerUser1006, customer001, /orgs/2/sites/site003
+g, customerUser1006, customer001, /orgs/2/sites/site004
+g, customerUser1006, customer001, /orgs/2/sites/site005
+
+g, customerUser1007, customer001, /orgs/2/sites/site001
+g, customerUser1007, customer001, /orgs/2/sites/site002
+g, customerUser1007, customer001, /orgs/2/sites/site003
+g, customerUser1007, customer001, /orgs/2/sites/site004
+g, customerUser1007, customer001, /orgs/2/sites/site005
+
+g, customerUser1008, customer001, /orgs/2/sites/site001
+g, customerUser1008, customer001, /orgs/2/sites/site002
+g, customerUser1008, customer001, /orgs/2/sites/site003
+g, customerUser1008, customer001, /orgs/2/sites/site004
+g, customerUser1008, customer001, /orgs/2/sites/site005
+
+g, customerUser1009, customer001, /orgs/2/sites/site001
+g, customerUser1009, customer001, /orgs/2/sites/site002
+g, customerUser1009, customer001, /orgs/2/sites/site003
+g, customerUser1009, customer001, /orgs/2/sites/site004
+g, customerUser1009, customer001, /orgs/2/sites/site005
+
+g, customerUser1010, customer001, /orgs/2/sites/site001
+g, customerUser1010, customer001, /orgs/2/sites/site002
+g, customerUser1010, customer001, /orgs/2/sites/site003
+g, customerUser1010, customer001, /orgs/2/sites/site004
+g, customerUser1010, customer001, /orgs/2/sites/site005
+
+g, customerUser1011, customer001, /orgs/2/sites/site001
+g, customerUser1011, customer001, /orgs/2/sites/site002
+g, customerUser1011, customer001, /orgs/2/sites/site003
+g, customerUser1011, customer001, /orgs/2/sites/site004
+g, customerUser1011, customer001, /orgs/2/sites/site005
+
+g, customerUser1012, customer001, /orgs/2/sites/site001
+g, customerUser1012, customer001, /orgs/2/sites/site002
+g, customerUser1012, customer001, /orgs/2/sites/site003
+g, customerUser1012, customer001, /orgs/2/sites/site004
+g, customerUser1012, customer001, /orgs/2/sites/site005
+
+g, customerUser1013, customer001, /orgs/2/sites/site001
+g, customerUser1013, customer001, /orgs/2/sites/site002
+g, customerUser1013, customer001, /orgs/2/sites/site003
+g, customerUser1013, customer001, /orgs/2/sites/site004
+g, customerUser1013, customer001, /orgs/2/sites/site005
+
+g, customerUser1014, customer001, /orgs/2/sites/site001
+g, customerUser1014, customer001, /orgs/2/sites/site002
+g, customerUser1014, customer001, /orgs/2/sites/site003
+g, customerUser1014, customer001, /orgs/2/sites/site004
+g, customerUser1014, customer001, /orgs/2/sites/site005
+
+g, customerUser1015, customer001, /orgs/2/sites/site001
+g, customerUser1015, customer001, /orgs/2/sites/site002
+g, customerUser1015, customer001, /orgs/2/sites/site003
+g, customerUser1015, customer001, /orgs/2/sites/site004
+g, customerUser1015, customer001, /orgs/2/sites/site005
+
+g, customerUser1016, customer001, /orgs/2/sites/site001
+g, customerUser1016, customer001, /orgs/2/sites/site002
+g, customerUser1016, customer001, /orgs/2/sites/site003
+g, customerUser1016, customer001, /orgs/2/sites/site004
+g, customerUser1016, customer001, /orgs/2/sites/site005
+
+g, customerUser1017, customer001, /orgs/2/sites/site001
+g, customerUser1017, customer001, /orgs/2/sites/site002
+g, customerUser1017, customer001, /orgs/2/sites/site003
+g, customerUser1017, customer001, /orgs/2/sites/site004
+g, customerUser1017, customer001, /orgs/2/sites/site005
+
+g, customerUser1018, customer001, /orgs/2/sites/site001
+g, customerUser1018, customer001, /orgs/2/sites/site002
+g, customerUser1018, customer001, /orgs/2/sites/site003
+g, customerUser1018, customer001, /orgs/2/sites/site004
+g, customerUser1018, customer001, /orgs/2/sites/site005
+
+g, customerUser1019, customer001, /orgs/2/sites/site001
+g, customerUser1019, customer001, /orgs/2/sites/site002
+g, customerUser1019, customer001, /orgs/2/sites/site003
+g, customerUser1019, customer001, /orgs/2/sites/site004
+g, customerUser1019, customer001, /orgs/2/sites/site005
+
+g, customerUser1020, customer001, /orgs/2/sites/site001
+g, customerUser1020, customer001, /orgs/2/sites/site002
+g, customerUser1020, customer001, /orgs/2/sites/site003
+g, customerUser1020, customer001, /orgs/2/sites/site004
+g, customerUser1020, customer001, /orgs/2/sites/site005
+
+g, customerUser1021, customer001, /orgs/2/sites/site001
+g, customerUser1021, customer001, /orgs/2/sites/site002
+g, customerUser1021, customer001, /orgs/2/sites/site003
+g, customerUser1021, customer001, /orgs/2/sites/site004
+g, customerUser1021, customer001, /orgs/2/sites/site005
+
+g, customerUser1022, customer001, /orgs/2/sites/site001
+g, customerUser1022, customer001, /orgs/2/sites/site002
+g, customerUser1022, customer001, /orgs/2/sites/site003
+g, customerUser1022, customer001, /orgs/2/sites/site004
+g, customerUser1022, customer001, /orgs/2/sites/site005
+
+g, customerUser1023, customer001, /orgs/2/sites/site001
+g, customerUser1023, customer001, /orgs/2/sites/site002
+g, customerUser1023, customer001, /orgs/2/sites/site003
+g, customerUser1023, customer001, /orgs/2/sites/site004
+g, customerUser1023, customer001, /orgs/2/sites/site005
+
+g, customerUser1024, customer001, /orgs/2/sites/site001
+g, customerUser1024, customer001, /orgs/2/sites/site002
+g, customerUser1024, customer001, /orgs/2/sites/site003
+g, customerUser1024, customer001, /orgs/2/sites/site004
+g, customerUser1024, customer001, /orgs/2/sites/site005
+
+g, customerUser1025, customer001, /orgs/2/sites/site001
+g, customerUser1025, customer001, /orgs/2/sites/site002
+g, customerUser1025, customer001, /orgs/2/sites/site003
+g, customerUser1025, customer001, /orgs/2/sites/site004
+g, customerUser1025, customer001, /orgs/2/sites/site005
+
+g, customerUser1026, customer001, /orgs/2/sites/site001
+g, customerUser1026, customer001, /orgs/2/sites/site002
+g, customerUser1026, customer001, /orgs/2/sites/site003
+g, customerUser1026, customer001, /orgs/2/sites/site004
+g, customerUser1026, customer001, /orgs/2/sites/site005
+
+g, customerUser1027, customer001, /orgs/2/sites/site001
+g, customerUser1027, customer001, /orgs/2/sites/site002
+g, customerUser1027, customer001, /orgs/2/sites/site003
+g, customerUser1027, customer001, /orgs/2/sites/site004
+g, customerUser1027, customer001, /orgs/2/sites/site005
+
+g, customerUser1028, customer001, /orgs/2/sites/site001
+g, customerUser1028, customer001, /orgs/2/sites/site002
+g, customerUser1028, customer001, /orgs/2/sites/site003
+g, customerUser1028, customer001, /orgs/2/sites/site004
+g, customerUser1028, customer001, /orgs/2/sites/site005
+
+g, customerUser1029, customer001, /orgs/2/sites/site001
+g, customerUser1029, customer001, /orgs/2/sites/site002
+g, customerUser1029, customer001, /orgs/2/sites/site003
+g, customerUser1029, customer001, /orgs/2/sites/site004
+g, customerUser1029, customer001, /orgs/2/sites/site005
+
+g, customerUser1030, customer001, /orgs/2/sites/site001
+g, customerUser1030, customer001, /orgs/2/sites/site002
+g, customerUser1030, customer001, /orgs/2/sites/site003
+g, customerUser1030, customer001, /orgs/2/sites/site004
+g, customerUser1030, customer001, /orgs/2/sites/site005
+
+g, customerUser1031, customer001, /orgs/2/sites/site001
+g, customerUser1031, customer001, /orgs/2/sites/site002
+g, customerUser1031, customer001, /orgs/2/sites/site003
+g, customerUser1031, customer001, /orgs/2/sites/site004
+g, customerUser1031, customer001, /orgs/2/sites/site005
+
+g, customerUser1032, customer001, /orgs/2/sites/site001
+g, customerUser1032, customer001, /orgs/2/sites/site002
+g, customerUser1032, customer001, /orgs/2/sites/site003
+g, customerUser1032, customer001, /orgs/2/sites/site004
+g, customerUser1032, customer001, /orgs/2/sites/site005
+
+g, customerUser1033, customer001, /orgs/2/sites/site001
+g, customerUser1033, customer001, /orgs/2/sites/site002
+g, customerUser1033, customer001, /orgs/2/sites/site003
+g, customerUser1033, customer001, /orgs/2/sites/site004
+g, customerUser1033, customer001, /orgs/2/sites/site005
+
+g, customerUser1034, customer001, /orgs/2/sites/site001
+g, customerUser1034, customer001, /orgs/2/sites/site002
+g, customerUser1034, customer001, /orgs/2/sites/site003
+g, customerUser1034, customer001, /orgs/2/sites/site004
+g, customerUser1034, customer001, /orgs/2/sites/site005
+
+g, customerUser1035, customer001, /orgs/2/sites/site001
+g, customerUser1035, customer001, /orgs/2/sites/site002
+g, customerUser1035, customer001, /orgs/2/sites/site003
+g, customerUser1035, customer001, /orgs/2/sites/site004
+g, customerUser1035, customer001, /orgs/2/sites/site005
+
+g, customerUser1036, customer001, /orgs/2/sites/site001
+g, customerUser1036, customer001, /orgs/2/sites/site002
+g, customerUser1036, customer001, /orgs/2/sites/site003
+g, customerUser1036, customer001, /orgs/2/sites/site004
+g, customerUser1036, customer001, /orgs/2/sites/site005
+
+g, customerUser1037, customer001, /orgs/2/sites/site001
+g, customerUser1037, customer001, /orgs/2/sites/site002
+g, customerUser1037, customer001, /orgs/2/sites/site003
+g, customerUser1037, customer001, /orgs/2/sites/site004
+g, customerUser1037, customer001, /orgs/2/sites/site005
+
+g, customerUser1038, customer001, /orgs/2/sites/site001
+g, customerUser1038, customer001, /orgs/2/sites/site002
+g, customerUser1038, customer001, /orgs/2/sites/site003
+g, customerUser1038, customer001, /orgs/2/sites/site004
+g, customerUser1038, customer001, /orgs/2/sites/site005
+
+g, customerUser1039, customer001, /orgs/2/sites/site001
+g, customerUser1039, customer001, /orgs/2/sites/site002
+g, customerUser1039, customer001, /orgs/2/sites/site003
+g, customerUser1039, customer001, /orgs/2/sites/site004
+g, customerUser1039, customer001, /orgs/2/sites/site005
+
+g, customerUser1040, customer001, /orgs/2/sites/site001
+g, customerUser1040, customer001, /orgs/2/sites/site002
+g, customerUser1040, customer001, /orgs/2/sites/site003
+g, customerUser1040, customer001, /orgs/2/sites/site004
+g, customerUser1040, customer001, /orgs/2/sites/site005
+
+g, customerUser1041, customer001, /orgs/2/sites/site001
+g, customerUser1041, customer001, /orgs/2/sites/site002
+g, customerUser1041, customer001, /orgs/2/sites/site003
+g, customerUser1041, customer001, /orgs/2/sites/site004
+g, customerUser1041, customer001, /orgs/2/sites/site005
+
+g, customerUser1042, customer001, /orgs/2/sites/site001
+g, customerUser1042, customer001, /orgs/2/sites/site002
+g, customerUser1042, customer001, /orgs/2/sites/site003
+g, customerUser1042, customer001, /orgs/2/sites/site004
+g, customerUser1042, customer001, /orgs/2/sites/site005
+
+g, customerUser1043, customer001, /orgs/2/sites/site001
+g, customerUser1043, customer001, /orgs/2/sites/site002
+g, customerUser1043, customer001, /orgs/2/sites/site003
+g, customerUser1043, customer001, /orgs/2/sites/site004
+g, customerUser1043, customer001, /orgs/2/sites/site005
+
+g, customerUser1044, customer001, /orgs/2/sites/site001
+g, customerUser1044, customer001, /orgs/2/sites/site002
+g, customerUser1044, customer001, /orgs/2/sites/site003
+g, customerUser1044, customer001, /orgs/2/sites/site004
+g, customerUser1044, customer001, /orgs/2/sites/site005
+
+g, customerUser1045, customer001, /orgs/2/sites/site001
+g, customerUser1045, customer001, /orgs/2/sites/site002
+g, customerUser1045, customer001, /orgs/2/sites/site003
+g, customerUser1045, customer001, /orgs/2/sites/site004
+g, customerUser1045, customer001, /orgs/2/sites/site005
+
+g, customerUser1046, customer001, /orgs/2/sites/site001
+g, customerUser1046, customer001, /orgs/2/sites/site002
+g, customerUser1046, customer001, /orgs/2/sites/site003
+g, customerUser1046, customer001, /orgs/2/sites/site004
+g, customerUser1046, customer001, /orgs/2/sites/site005
+
+g, customerUser1047, customer001, /orgs/2/sites/site001
+g, customerUser1047, customer001, /orgs/2/sites/site002
+g, customerUser1047, customer001, /orgs/2/sites/site003
+g, customerUser1047, customer001, /orgs/2/sites/site004
+g, customerUser1047, customer001, /orgs/2/sites/site005
+
+g, customerUser1048, customer001, /orgs/2/sites/site001
+g, customerUser1048, customer001, /orgs/2/sites/site002
+g, customerUser1048, customer001, /orgs/2/sites/site003
+g, customerUser1048, customer001, /orgs/2/sites/site004
+g, customerUser1048, customer001, /orgs/2/sites/site005
+
+g, customerUser1049, customer001, /orgs/2/sites/site001
+g, customerUser1049, customer001, /orgs/2/sites/site002
+g, customerUser1049, customer001, /orgs/2/sites/site003
+g, customerUser1049, customer001, /orgs/2/sites/site004
+g, customerUser1049, customer001, /orgs/2/sites/site005
+
+g, customerUser1050, customer001, /orgs/2/sites/site001
+g, customerUser1050, customer001, /orgs/2/sites/site002
+g, customerUser1050, customer001, /orgs/2/sites/site003
+g, customerUser1050, customer001, /orgs/2/sites/site004
+g, customerUser1050, customer001, /orgs/2/sites/site005
+
+# Group - customer001, / org2
+g, customerUser2001, customer001, /orgs/2/sites/site001
+g, customerUser2001, customer001, /orgs/2/sites/site002
+g, customerUser2001, customer001, /orgs/2/sites/site003
+g, customerUser2001, customer001, /orgs/2/sites/site004
+g, customerUser2001, customer001, /orgs/2/sites/site005
+
+g, customerUser2001, customer001, /orgs/2/sites/site001
+g, customerUser2001, customer001, /orgs/2/sites/site002
+g, customerUser2001, customer001, /orgs/2/sites/site003
+g, customerUser2001, customer001, /orgs/2/sites/site004
+g, customerUser2001, customer001, /orgs/2/sites/site005
+
+g, customerUser2003, customer001, /orgs/2/sites/site001
+g, customerUser2003, customer001, /orgs/2/sites/site002
+g, customerUser2003, customer001, /orgs/2/sites/site003
+g, customerUser2003, customer001, /orgs/2/sites/site004
+g, customerUser2003, customer001, /orgs/2/sites/site005
+
+g, customerUser2004, customer001, /orgs/2/sites/site001
+g, customerUser2004, customer001, /orgs/2/sites/site002
+g, customerUser2004, customer001, /orgs/2/sites/site003
+g, customerUser2004, customer001, /orgs/2/sites/site004
+g, customerUser2004, customer001, /orgs/2/sites/site005
+
+g, customerUser2005, customer001, /orgs/2/sites/site001
+g, customerUser2005, customer001, /orgs/2/sites/site002
+g, customerUser2005, customer001, /orgs/2/sites/site003
+g, customerUser2005, customer001, /orgs/2/sites/site004
+g, customerUser2005, customer001, /orgs/2/sites/site005
+
+g, customerUser2006, customer001, /orgs/2/sites/site001
+g, customerUser2006, customer001, /orgs/2/sites/site002
+g, customerUser2006, customer001, /orgs/2/sites/site003
+g, customerUser2006, customer001, /orgs/2/sites/site004
+g, customerUser2006, customer001, /orgs/2/sites/site005
+
+g, customerUser2007, customer001, /orgs/2/sites/site001
+g, customerUser2007, customer001, /orgs/2/sites/site002
+g, customerUser2007, customer001, /orgs/2/sites/site003
+g, customerUser2007, customer001, /orgs/2/sites/site004
+g, customerUser2007, customer001, /orgs/2/sites/site005
+
+g, customerUser2008, customer001, /orgs/2/sites/site001
+g, customerUser2008, customer001, /orgs/2/sites/site002
+g, customerUser2008, customer001, /orgs/2/sites/site003
+g, customerUser2008, customer001, /orgs/2/sites/site004
+g, customerUser2008, customer001, /orgs/2/sites/site005
+
+g, customerUser2009, customer001, /orgs/2/sites/site001
+g, customerUser2009, customer001, /orgs/2/sites/site002
+g, customerUser2009, customer001, /orgs/2/sites/site003
+g, customerUser2009, customer001, /orgs/2/sites/site004
+g, customerUser2009, customer001, /orgs/2/sites/site005
+
+g, customerUser2010, customer001, /orgs/2/sites/site001
+g, customerUser2010, customer001, /orgs/2/sites/site002
+g, customerUser2010, customer001, /orgs/2/sites/site003
+g, customerUser2010, customer001, /orgs/2/sites/site004
+g, customerUser2010, customer001, /orgs/2/sites/site005
+
+g, customerUser2011, customer001, /orgs/2/sites/site001
+g, customerUser2011, customer001, /orgs/2/sites/site002
+g, customerUser2011, customer001, /orgs/2/sites/site003
+g, customerUser2011, customer001, /orgs/2/sites/site004
+g, customerUser2011, customer001, /orgs/2/sites/site005
+
+g, customerUser2012, customer001, /orgs/2/sites/site001
+g, customerUser2012, customer001, /orgs/2/sites/site002
+g, customerUser2012, customer001, /orgs/2/sites/site003
+g, customerUser2012, customer001, /orgs/2/sites/site004
+g, customerUser2012, customer001, /orgs/2/sites/site005
+
+g, customerUser2013, customer001, /orgs/2/sites/site001
+g, customerUser2013, customer001, /orgs/2/sites/site002
+g, customerUser2013, customer001, /orgs/2/sites/site003
+g, customerUser2013, customer001, /orgs/2/sites/site004
+g, customerUser2013, customer001, /orgs/2/sites/site005
+
+g, customerUser2014, customer001, /orgs/2/sites/site001
+g, customerUser2014, customer001, /orgs/2/sites/site002
+g, customerUser2014, customer001, /orgs/2/sites/site003
+g, customerUser2014, customer001, /orgs/2/sites/site004
+g, customerUser2014, customer001, /orgs/2/sites/site005
+
+g, customerUser2015, customer001, /orgs/2/sites/site001
+g, customerUser2015, customer001, /orgs/2/sites/site002
+g, customerUser2015, customer001, /orgs/2/sites/site003
+g, customerUser2015, customer001, /orgs/2/sites/site004
+g, customerUser2015, customer001, /orgs/2/sites/site005
+
+g, customerUser2016, customer001, /orgs/2/sites/site001
+g, customerUser2016, customer001, /orgs/2/sites/site002
+g, customerUser2016, customer001, /orgs/2/sites/site003
+g, customerUser2016, customer001, /orgs/2/sites/site004
+g, customerUser2016, customer001, /orgs/2/sites/site005
+
+g, customerUser2017, customer001, /orgs/2/sites/site001
+g, customerUser2017, customer001, /orgs/2/sites/site002
+g, customerUser2017, customer001, /orgs/2/sites/site003
+g, customerUser2017, customer001, /orgs/2/sites/site004
+g, customerUser2017, customer001, /orgs/2/sites/site005
+
+g, customerUser2018, customer001, /orgs/2/sites/site001
+g, customerUser2018, customer001, /orgs/2/sites/site002
+g, customerUser2018, customer001, /orgs/2/sites/site003
+g, customerUser2018, customer001, /orgs/2/sites/site004
+g, customerUser2018, customer001, /orgs/2/sites/site005
+
+g, customerUser2019, customer001, /orgs/2/sites/site001
+g, customerUser2019, customer001, /orgs/2/sites/site002
+g, customerUser2019, customer001, /orgs/2/sites/site003
+g, customerUser2019, customer001, /orgs/2/sites/site004
+g, customerUser2019, customer001, /orgs/2/sites/site005
+
+g, customerUser2020, customer001, /orgs/2/sites/site001
+g, customerUser2020, customer001, /orgs/2/sites/site002
+g, customerUser2020, customer001, /orgs/2/sites/site003
+g, customerUser2020, customer001, /orgs/2/sites/site004
+g, customerUser2020, customer001, /orgs/2/sites/site005
+
+g, customerUser2021, customer001, /orgs/2/sites/site001
+g, customerUser2021, customer001, /orgs/2/sites/site002
+g, customerUser2021, customer001, /orgs/2/sites/site003
+g, customerUser2021, customer001, /orgs/2/sites/site004
+g, customerUser2021, customer001, /orgs/2/sites/site005
+
+g, customerUser2022, customer001, /orgs/2/sites/site001
+g, customerUser2022, customer001, /orgs/2/sites/site002
+g, customerUser2022, customer001, /orgs/2/sites/site003
+g, customerUser2022, customer001, /orgs/2/sites/site004
+g, customerUser2022, customer001, /orgs/2/sites/site005
+
+g, customerUser2023, customer001, /orgs/2/sites/site001
+g, customerUser2023, customer001, /orgs/2/sites/site002
+g, customerUser2023, customer001, /orgs/2/sites/site003
+g, customerUser2023, customer001, /orgs/2/sites/site004
+g, customerUser2023, customer001, /orgs/2/sites/site005
+
+g, customerUser2024, customer001, /orgs/2/sites/site001
+g, customerUser2024, customer001, /orgs/2/sites/site002
+g, customerUser2024, customer001, /orgs/2/sites/site003
+g, customerUser2024, customer001, /orgs/2/sites/site004
+g, customerUser2024, customer001, /orgs/2/sites/site005
+
+g, customerUser2025, customer001, /orgs/2/sites/site001
+g, customerUser2025, customer001, /orgs/2/sites/site002
+g, customerUser2025, customer001, /orgs/2/sites/site003
+g, customerUser2025, customer001, /orgs/2/sites/site004
+g, customerUser2025, customer001, /orgs/2/sites/site005
+
+g, customerUser2026, customer001, /orgs/2/sites/site001
+g, customerUser2026, customer001, /orgs/2/sites/site002
+g, customerUser2026, customer001, /orgs/2/sites/site003
+g, customerUser2026, customer001, /orgs/2/sites/site004
+g, customerUser2026, customer001, /orgs/2/sites/site005
+
+g, customerUser2027, customer001, /orgs/2/sites/site001
+g, customerUser2027, customer001, /orgs/2/sites/site002
+g, customerUser2027, customer001, /orgs/2/sites/site003
+g, customerUser2027, customer001, /orgs/2/sites/site004
+g, customerUser2027, customer001, /orgs/2/sites/site005
+
+g, customerUser2028, customer001, /orgs/2/sites/site001
+g, customerUser2028, customer001, /orgs/2/sites/site002
+g, customerUser2028, customer001, /orgs/2/sites/site003
+g, customerUser2028, customer001, /orgs/2/sites/site004
+g, customerUser2028, customer001, /orgs/2/sites/site005
+
+g, customerUser2029, customer001, /orgs/2/sites/site001
+g, customerUser2029, customer001, /orgs/2/sites/site002
+g, customerUser2029, customer001, /orgs/2/sites/site003
+g, customerUser2029, customer001, /orgs/2/sites/site004
+g, customerUser2029, customer001, /orgs/2/sites/site005
+
+g, customerUser2030, customer001, /orgs/2/sites/site001
+g, customerUser2030, customer001, /orgs/2/sites/site002
+g, customerUser2030, customer001, /orgs/2/sites/site003
+g, customerUser2030, customer001, /orgs/2/sites/site004
+g, customerUser2030, customer001, /orgs/2/sites/site005
+
+g, customerUser2031, customer001, /orgs/2/sites/site001
+g, customerUser2031, customer001, /orgs/2/sites/site002
+g, customerUser2031, customer001, /orgs/2/sites/site003
+g, customerUser2031, customer001, /orgs/2/sites/site004
+g, customerUser2031, customer001, /orgs/2/sites/site005
+
+g, customerUser2032, customer001, /orgs/2/sites/site001
+g, customerUser2032, customer001, /orgs/2/sites/site002
+g, customerUser2032, customer001, /orgs/2/sites/site003
+g, customerUser2032, customer001, /orgs/2/sites/site004
+g, customerUser2032, customer001, /orgs/2/sites/site005
+
+g, customerUser2033, customer001, /orgs/2/sites/site001
+g, customerUser2033, customer001, /orgs/2/sites/site002
+g, customerUser2033, customer001, /orgs/2/sites/site003
+g, customerUser2033, customer001, /orgs/2/sites/site004
+g, customerUser2033, customer001, /orgs/2/sites/site005
+
+g, customerUser2034, customer001, /orgs/2/sites/site001
+g, customerUser2034, customer001, /orgs/2/sites/site002
+g, customerUser2034, customer001, /orgs/2/sites/site003
+g, customerUser2034, customer001, /orgs/2/sites/site004
+g, customerUser2034, customer001, /orgs/2/sites/site005
+
+g, customerUser2035, customer001, /orgs/2/sites/site001
+g, customerUser2035, customer001, /orgs/2/sites/site002
+g, customerUser2035, customer001, /orgs/2/sites/site003
+g, customerUser2035, customer001, /orgs/2/sites/site004
+g, customerUser2035, customer001, /orgs/2/sites/site005
+
+g, customerUser2036, customer001, /orgs/2/sites/site001
+g, customerUser2036, customer001, /orgs/2/sites/site002
+g, customerUser2036, customer001, /orgs/2/sites/site003
+g, customerUser2036, customer001, /orgs/2/sites/site004
+g, customerUser2036, customer001, /orgs/2/sites/site005
+
+g, customerUser2037, customer001, /orgs/2/sites/site001
+g, customerUser2037, customer001, /orgs/2/sites/site002
+g, customerUser2037, customer001, /orgs/2/sites/site003
+g, customerUser2037, customer001, /orgs/2/sites/site004
+g, customerUser2037, customer001, /orgs/2/sites/site005
+
+g, customerUser2038, customer001, /orgs/2/sites/site001
+g, customerUser2038, customer001, /orgs/2/sites/site002
+g, customerUser2038, customer001, /orgs/2/sites/site003
+g, customerUser2038, customer001, /orgs/2/sites/site004
+g, customerUser2038, customer001, /orgs/2/sites/site005
+
+g, customerUser2039, customer001, /orgs/2/sites/site001
+g, customerUser2039, customer001, /orgs/2/sites/site002
+g, customerUser2039, customer001, /orgs/2/sites/site003
+g, customerUser2039, customer001, /orgs/2/sites/site004
+g, customerUser2039, customer001, /orgs/2/sites/site005
+
+g, customerUser2040, customer001, /orgs/2/sites/site001
+g, customerUser2040, customer001, /orgs/2/sites/site002
+g, customerUser2040, customer001, /orgs/2/sites/site003
+g, customerUser2040, customer001, /orgs/2/sites/site004
+g, customerUser2040, customer001, /orgs/2/sites/site005
+
+g, customerUser2041, customer001, /orgs/2/sites/site001
+g, customerUser2041, customer001, /orgs/2/sites/site002
+g, customerUser2041, customer001, /orgs/2/sites/site003
+g, customerUser2041, customer001, /orgs/2/sites/site004
+g, customerUser2041, customer001, /orgs/2/sites/site005
+
+g, customerUser2042, customer001, /orgs/2/sites/site001
+g, customerUser2042, customer001, /orgs/2/sites/site002
+g, customerUser2042, customer001, /orgs/2/sites/site003
+g, customerUser2042, customer001, /orgs/2/sites/site004
+g, customerUser2042, customer001, /orgs/2/sites/site005
+
+g, customerUser2043, customer001, /orgs/2/sites/site001
+g, customerUser2043, customer001, /orgs/2/sites/site002
+g, customerUser2043, customer001, /orgs/2/sites/site003
+g, customerUser2043, customer001, /orgs/2/sites/site004
+g, customerUser2043, customer001, /orgs/2/sites/site005
+
+g, customerUser2044, customer001, /orgs/2/sites/site001
+g, customerUser2044, customer001, /orgs/2/sites/site002
+g, customerUser2044, customer001, /orgs/2/sites/site003
+g, customerUser2044, customer001, /orgs/2/sites/site004
+g, customerUser2044, customer001, /orgs/2/sites/site005
+
+g, customerUser2045, customer001, /orgs/2/sites/site001
+g, customerUser2045, customer001, /orgs/2/sites/site002
+g, customerUser2045, customer001, /orgs/2/sites/site003
+g, customerUser2045, customer001, /orgs/2/sites/site004
+g, customerUser2045, customer001, /orgs/2/sites/site005
+
+g, customerUser2046, customer001, /orgs/2/sites/site001
+g, customerUser2046, customer001, /orgs/2/sites/site002
+g, customerUser2046, customer001, /orgs/2/sites/site003
+g, customerUser2046, customer001, /orgs/2/sites/site004
+g, customerUser2046, customer001, /orgs/2/sites/site005
+
+g, customerUser2047, customer001, /orgs/2/sites/site001
+g, customerUser2047, customer001, /orgs/2/sites/site002
+g, customerUser2047, customer001, /orgs/2/sites/site003
+g, customerUser2047, customer001, /orgs/2/sites/site004
+g, customerUser2047, customer001, /orgs/2/sites/site005
+
+g, customerUser2048, customer001, /orgs/2/sites/site001
+g, customerUser2048, customer001, /orgs/2/sites/site002
+g, customerUser2048, customer001, /orgs/2/sites/site003
+g, customerUser2048, customer001, /orgs/2/sites/site004
+g, customerUser2048, customer001, /orgs/2/sites/site005
+
+g, customerUser2049, customer001, /orgs/2/sites/site001
+g, customerUser2049, customer001, /orgs/2/sites/site002
+g, customerUser2049, customer001, /orgs/2/sites/site003
+g, customerUser2049, customer001, /orgs/2/sites/site004
+g, customerUser2049, customer001, /orgs/2/sites/site005
+
+g, customerUser2050, customer001, /orgs/2/sites/site001
+g, customerUser2050, customer001, /orgs/2/sites/site002
+g, customerUser2050, customer001, /orgs/2/sites/site003
+g, customerUser2050, customer001, /orgs/2/sites/site004
+g, customerUser2050, customer001, /orgs/2/sites/site005
\ No newline at end of file
diff --git a/examples/priority_indeterminate_policy.csv b/examples/priority_indeterminate_policy.csv
index c4e67cd99..974aa27e1 100644
--- a/examples/priority_indeterminate_policy.csv
+++ b/examples/priority_indeterminate_policy.csv
@@ -1 +1 @@
-p, alice, data1, read, intdeterminate
\ No newline at end of file
+p, alice, data1, read, indeterminate
\ 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 000000000..662aeb803
--- /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 000000000..5df75b279
--- /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 000000000..5071fa771
--- /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_enforce_context.csv b/examples/priority_policy_enforce_context.csv
new file mode 100644
index 000000000..756de5c55
--- /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 000000000..0fec82c51
--- /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 000000000..a861e2ba7
--- /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_in_multi_line.conf b/examples/rbac_model_in_multi_line.conf
new file mode 100644
index 000000000..17771b67e
--- /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 000000000..227d1494b
--- /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 000000000..6ff819feb
--- /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_with_all_pattern_model.conf b/examples/rbac_with_all_pattern_model.conf
new file mode 100644
index 000000000..045bfa575
--- /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 000000000..8097be8a3
--- /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 000000000..2cf26ef42
--- /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 000000000..7c620321f
--- /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_different_types_of_roles_model.conf b/examples/rbac_with_different_types_of_roles_model.conf
new file mode 100644
index 000000000..069f2348f
--- /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 000000000..67d090be3
--- /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 000000000..774e44188
--- /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 000000000..783f721e4
--- /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 000000000..e1ab8a41b
--- /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 000000000..8234b64be
--- /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_domains_conditional_model.conf b/examples/rbac_with_domains_conditional_model.conf
new file mode 100644
index 000000000..7dae4ada6
--- /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 000000000..9f300815c
--- /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_policy2.csv b/examples/rbac_with_domains_policy2.csv
new file mode 100644
index 000000000..baa06f063
--- /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 000000000..f72299868
--- /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 000000000..45d91739a
--- /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 000000000..d4581acda
--- /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 000000000..0afb588d3
--- /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
index dcf03a1e2..771f33dc5 100644
--- a/examples/rbac_with_not_deny_model.conf
+++ b/examples/rbac_with_not_deny_model.conf
@@ -1,14 +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]
+[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 000000000..84580d90f
--- /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 000000000..bbe76872a
--- /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_policy.csv b/examples/rbac_with_resource_roles_policy.csv
index 889084705..b1d36daf3 100644
--- a/examples/rbac_with_resource_roles_policy.csv
+++ b/examples/rbac_with_resource_roles_policy.csv
@@ -1,6 +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 000000000..feeae160b
--- /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 000000000..c8f77e7bc
--- /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 000000000..763095022
--- /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 000000000..998652b49
--- /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 000000000..77b8c4ebc
--- /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 000000000..84ec518c9
--- /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 000000000..24a44223d
--- /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 000000000..c4859ecd3
--- /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 000000000..f99a38599
--- /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
index 760d78601..0b97d522f 100644
--- a/filter_test.go
+++ b/filter_test.go
@@ -17,14 +17,28 @@ package casbin
import (
"testing"
- "github.com/casbin/casbin/persist/file-adapter"
+ 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()
+ e, _ := NewEnforcer()
adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
- e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)
+ _ = 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)
@@ -52,11 +66,91 @@ func TestLoadFilteredPolicy(t *testing.T) {
}
}
+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()
+ e, _ := NewEnforcer()
adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
- e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)
+ _ = 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")
@@ -64,10 +158,10 @@ func TestFilteredPolicyInvalidFilter(t *testing.T) {
}
func TestFilteredPolicyEmptyFilter(t *testing.T) {
- e := NewEnforcer()
+ e, _ := NewEnforcer()
adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
- e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)
+ _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)
if err := e.LoadFilteredPolicy(nil); err != nil {
t.Errorf("unexpected error in LoadFilteredPolicy: %v", err)
@@ -81,7 +175,7 @@ func TestFilteredPolicyEmptyFilter(t *testing.T) {
}
func TestUnsupportedFilteredPolicy(t *testing.T) {
- e := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv")
+ e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv")
err := e.LoadFilteredPolicy(&fileadapter.Filter{
P: []string{"", "domain1"},
@@ -93,21 +187,21 @@ func TestUnsupportedFilteredPolicy(t *testing.T) {
}
func TestFilteredAdapterEmptyFilepath(t *testing.T) {
- e := NewEnforcer()
+ e, _ := NewEnforcer()
adapter := fileadapter.NewFilteredAdapter("")
- e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)
+ _ = 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")
+ if err := e.LoadFilteredPolicy(nil); err != nil {
+ t.Errorf("unexpected error in LoadFilteredPolicy: %v", err)
}
}
func TestFilteredAdapterInvalidFilepath(t *testing.T) {
- e := NewEnforcer()
+ e, _ := NewEnforcer()
adapter := fileadapter.NewFilteredAdapter("examples/does_not_exist_policy.csv")
- e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter)
+ _ = 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 000000000..101a23a5d
--- /dev/null
+++ b/frontend.go
@@ -0,0 +1,57 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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/util/log_util.go b/frontend_old.go
similarity index 54%
rename from util/log_util.go
rename to frontend_old.go
index eeb3bcac4..139b164fb 100644
--- a/util/log_util.go
+++ b/frontend_old.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The casbin Authors. All Rights Reserved.
+// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,23 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package util
+package casbin
-import "log"
+import "encoding/json"
-// EnableLog controls whether to print log to console.
-var EnableLog = true
-
-// LogPrint prints the log.
-func LogPrint(v ...interface{}) {
- if EnableLog {
- log.Print(v)
+func CasbinJsGetPermissionForUserOld(e IEnforcer, user string) ([]byte, error) {
+ policy, err := e.GetImplicitPermissionsForUser(user)
+ if err != nil {
+ return nil, err
}
-}
-
-// LogPrintf prints the log with the format.
-func LogPrintf(format string, v ...interface{}) {
- if EnableLog {
- log.Printf(format, v...)
+ 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 000000000..c55338924
--- /dev/null
+++ b/frontend_old_test.go
@@ -0,0 +1,93 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..d54b7729b
--- /dev/null
+++ b/frontend_test.go
@@ -0,0 +1,66 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+package casbin
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "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 := ioutil.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 := ioutil.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 000000000..c46f727d3
--- /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 000000000..2f3a1c775
--- /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
index c189b8898..7fd16323c 100644
--- a/internal_api.go
+++ b/internal_api.go
@@ -14,62 +14,539 @@
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) addPolicy(sec string, ptype string, rule []string) bool {
- ruleAdded := e.model.AddPolicy(sec, ptype, rule)
-
- if ruleAdded {
- if e.adapter != nil && e.autoSave {
- err := e.adapter.AddPolicy(sec, ptype, rule)
- if err != nil && err.Error() != "not implemented" {
- panic(err)
- } else if err == nil {
- if e.watcher != nil {
- e.watcher.Update()
- }
+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
}
}
}
- return ruleAdded
+ 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) removePolicy(sec string, ptype string, rule []string) bool {
- ruleRemoved := e.model.RemovePolicy(sec, ptype, rule)
-
- if ruleRemoved {
- if e.adapter != nil && e.autoSave {
- err := e.adapter.RemovePolicy(sec, ptype, rule)
- if err != nil && err.Error() != "not implemented" {
- panic(err)
- } else if err == nil {
- if e.watcher != nil {
- e.watcher.Update()
- }
+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
}
}
}
- return ruleRemoved
+ 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) removeFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) bool {
- ruleRemoved := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
-
- if ruleRemoved {
- if e.adapter != nil && e.autoSave {
- err := e.adapter.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
- if err != nil && err.Error() != "not implemented" {
- panic(err)
- } else if err == nil {
- if e.watcher != nil {
- e.watcher.Update()
- }
+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
}
}
}
- return ruleRemoved
+ 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 000000000..53d0693af
--- /dev/null
+++ b/lbac_test.go
@@ -0,0 +1,56 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..13ec21d99
--- /dev/null
+++ b/log/default_logger.go
@@ -0,0 +1,139 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..87153fdf2
--- /dev/null
+++ b/log/logger.go
@@ -0,0 +1,26 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..d18ec5288
--- /dev/null
+++ b/log/types.go
@@ -0,0 +1,60 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..08aa5fb7b
--- /dev/null
+++ b/logger_test.go
@@ -0,0 +1,248 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 01ca6fc19..7a8f768ee 100644
--- a/management_api.go
+++ b/management_api.go
@@ -14,93 +14,210 @@
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 {
- return e.GetAllNamedSubjects("p")
+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 {
- return e.model.GetValuesForFieldInPolicy("p", ptype, 0)
+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 {
- return e.GetAllNamedObjects("p")
+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 {
- return e.model.GetValuesForFieldInPolicy("p", ptype, 1)
+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 {
- return e.GetAllNamedActions("p")
+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 {
- return e.model.GetValuesForFieldInPolicy("p", ptype, 2)
+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 {
- return e.GetAllNamedRoles("g")
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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 {
+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)
+ }
+ 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 {
+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 {
+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)
}
@@ -116,68 +233,125 @@ func (e *Enforcer) HasNamedPolicy(ptype string, params ...interface{}) bool {
// 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 {
+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 {
- ruleAdded := false
+func (e *Enforcer) AddNamedPolicy(ptype string, params ...interface{}) (bool, error) {
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
- ruleAdded = e.addPolicy("p", ptype, strSlice)
- } else {
- policy := make([]string, 0)
- for _, param := range params {
- policy = append(policy, param.(string))
- }
-
- ruleAdded = e.addPolicy("p", ptype, policy)
+ 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 ruleAdded
+ 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 {
+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 {
+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 {
- ruleRemoved := false
+func (e *Enforcer) RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error) {
if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
- ruleRemoved = e.removePolicy("p", ptype, strSlice)
- } else {
- policy := make([]string, 0)
- for _, param := range params {
- policy = append(policy, param.(string))
- }
-
- ruleRemoved = e.removePolicy("p", ptype, policy)
+ return e.removePolicy("p", ptype, strSlice)
+ }
+ policy := make([]string, 0)
+ for _, param := range params {
+ policy = append(policy, param.(string))
}
- return ruleRemoved
+ 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 {
- return e.removeFilteredPolicy("p", ptype, fieldIndex, fieldValues...)
+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 {
+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 {
+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)
}
@@ -193,73 +367,151 @@ func (e *Enforcer) HasNamedGroupingPolicy(ptype string, params ...interface{}) b
// 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 {
+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 {
- ruleAdded := false
+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 = e.addPolicy("g", ptype, strSlice)
+ ruleAdded, err = e.addPolicy("g", ptype, strSlice)
} else {
policy := make([]string, 0)
for _, param := range params {
policy = append(policy, param.(string))
}
- ruleAdded = e.addPolicy("g", ptype, policy)
+ ruleAdded, err = e.addPolicy("g", ptype, policy)
}
- if e.autoBuildRoleLinks {
- e.BuildRoleLinks()
- }
- return ruleAdded
+ 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 {
+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 {
+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 {
- ruleRemoved := false
+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 = e.removePolicy("g", ptype, strSlice)
+ ruleRemoved, err = e.removePolicy("g", ptype, strSlice)
} else {
policy := make([]string, 0)
for _, param := range params {
policy = append(policy, param.(string))
}
- ruleRemoved = e.removePolicy("g", ptype, policy)
+ ruleRemoved, err = e.removePolicy("g", ptype, policy)
}
- if e.autoBuildRoleLinks {
- e.BuildRoleLinks()
- }
- return ruleRemoved
+ return ruleRemoved, err
}
-// 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 {
- ruleRemoved := e.removeFilteredPolicy("g", ptype, fieldIndex, fieldValues...)
+// 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)
+}
- if e.autoBuildRoleLinks {
- e.BuildRoleLinks()
- }
- return ruleRemoved
+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(ptype string, function func(args ...interface{}) (interface{}, error)) {
- e.fm.AddFunction(ptype, 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 000000000..2ebeda8fc
--- /dev/null
+++ b/management_api_b_test.go
@@ -0,0 +1,174 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+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")
+ }
+}
diff --git a/management_api_test.go b/management_api_test.go
index 3b5022bd2..620b39447 100644
--- a/management_api_test.go
+++ b/management_api_test.go
@@ -15,16 +15,19 @@
package casbin
import (
- "log"
"testing"
- "github.com/casbin/casbin/util"
+ "github.com/casbin/casbin/v3/util"
)
-func testStringList(t *testing.T, title string, f func() []string, res []string) {
+func testStringList(t *testing.T, title string, f func() ([]string, error), res []string) {
t.Helper()
- myRes := f()
- log.Print(title+": ", myRes)
+ 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)
@@ -32,38 +35,75 @@ func testStringList(t *testing.T, title string, f func() []string, res []string)
}
func TestGetList(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+ e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testStringList(t, "Subjects", e.GetAllSubjects, []string{"alice", "bob", "data2_admin"})
- testStringList(t, "Objeccts", e.GetAllObjects, []string{"data1", "data2"})
+ 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 := e.GetPolicy()
- log.Print("Policy: ", myRes)
+ myRes, err := e.GetPolicy()
+ if err != nil {
+ t.Error(err)
+ }
- if !util.Array2DEquals(res, myRes) {
+ 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 := e.GetFilteredPolicy(fieldIndex, fieldValues...)
- log.Print("Policy for ", util.ParamsToString(fieldValues...), ": ", myRes)
+ 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 := e.GetGroupingPolicy()
- log.Print("Grouping policy: ", myRes)
+ 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)
@@ -72,8 +112,12 @@ func testGetGroupingPolicy(t *testing.T, e *Enforcer, res [][]string) {
func testGetFilteredGroupingPolicy(t *testing.T, e *Enforcer, fieldIndex int, res [][]string, fieldValues ...string) {
t.Helper()
- myRes := e.GetFilteredGroupingPolicy(fieldIndex, fieldValues...)
- log.Print("Grouping policy for ", util.ParamsToString(fieldValues...), ": ", myRes)
+ 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)
@@ -82,8 +126,12 @@ func testGetFilteredGroupingPolicy(t *testing.T, e *Enforcer, fieldIndex int, re
func testHasPolicy(t *testing.T, e *Enforcer, policy []string, res bool) {
t.Helper()
- myRes := e.HasPolicy(policy)
- log.Print("Has policy ", util.ArrayToString(policy), ": ", myRes)
+ 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)
@@ -92,26 +140,20 @@ func testHasPolicy(t *testing.T, e *Enforcer, policy []string, res bool) {
func testHasGroupingPolicy(t *testing.T, e *Enforcer, policy []string, res bool) {
t.Helper()
- myRes := e.HasGroupingPolicy(policy)
- log.Print("Has grouping policy ", util.ArrayToString(policy), ": ", myRes)
-
- if res != myRes {
- t.Error("Has grouping policy ", util.ArrayToString(policy), ": ", myRes, ", supposed to be ", res)
+ myRes, err := e.HasGroupingPolicy(policy)
+ if err != nil {
+ t.Error(err)
}
-}
-func testHasGroupingPolicyStringInput(t *testing.T, e *Enforcer, policy1 string, policy2 string, res bool) {
- t.Helper()
- myRes := e.HasGroupingPolicy(policy1, policy2)
- log.Print("Has grouping policy ", policy1, policy2, ": ", myRes)
+ t.Log("Has grouping policy ", util.ArrayToString(policy), ": ", myRes)
if res != myRes {
- t.Error("Has grouping policy ", policy1, policy2, ": ", myRes, ", supposed to be ", res)
+ t.Error("Has grouping policy ", util.ArrayToString(policy), ": ", myRes, ", supposed to be ", res)
}
}
-func TestGetPolicy(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+func TestGetPolicyAPI(t *testing.T) {
+ e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
@@ -127,6 +169,13 @@ func TestGetPolicy(t *testing.T) {
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")
@@ -137,8 +186,7 @@ func TestGetPolicy(t *testing.T) {
testHasPolicy(t, e, []string{"alice", "data2", "read"}, false)
testHasPolicy(t, e, []string{"bob", "data3", "write"}, false)
- testGetGroupingPolicy(t, e, [][]string{
- {"alice", "data2_admin"}})
+ testGetGroupingPolicy(t, e, [][]string{{"alice", "data2_admin"}})
testGetFilteredGroupingPolicy(t, e, 0, [][]string{{"alice", "data2_admin"}}, "alice")
testGetFilteredGroupingPolicy(t, e, 0, [][]string{}, "bob")
@@ -149,13 +197,10 @@ func TestGetPolicy(t *testing.T) {
testHasGroupingPolicy(t, e, []string{"alice", "data2_admin"}, true)
testHasGroupingPolicy(t, e, []string{"bob", "data2_admin"}, false)
-
- testHasGroupingPolicyStringInput(t, e, "alice", "data2_admin", true)
- testHasGroupingPolicyStringInput(t, e, "bob", "data2_admin", false)
}
-func TestModifyPolicy(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+func TestModifyPolicyAPI(t *testing.T) {
+ e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testGetPolicy(t, e, [][]string{
{"alice", "data1", "read"},
@@ -163,61 +208,157 @@ func TestModifyPolicy(t *testing.T) {
{"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")
+ _, _ = 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)
+ _, _ = 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")
+ _, _ = 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 TestModifyGroupingPolicy(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+func TestModifyGroupingPolicyAPI(t *testing.T) {
+ e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
- testGetRoles(t, e, "alice", []string{"data2_admin"})
- testGetRoles(t, e, "bob", []string{})
- testGetRoles(t, e, "eve", []string{})
- testGetRoles(t, e, "non_exist", []string{})
+ 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.RemoveGroupingPolicy("alice", "data2_admin")
- e.AddGroupingPolicy("bob", "data1_admin")
- e.AddGroupingPolicy("eve", "data3_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, "alice", []string{})
- e.AddNamedGroupingPolicy("g", namedGroupingPolicy)
- testGetRoles(t, e, "alice", []string{"data2_admin"})
- e.RemoveNamedGroupingPolicy("g", namedGroupingPolicy)
-
- testGetRoles(t, e, "alice", []string{})
- testGetRoles(t, e, "bob", []string{"data1_admin"})
- testGetRoles(t, e, "eve", []string{"data3_admin"})
- testGetRoles(t, e, "non_exist", []string{})
-
- testGetUsers(t, e, "data1_admin", []string{"bob"})
- testGetUsers(t, e, "data2_admin", []string{})
- testGetUsers(t, e, "data3_admin", []string{"eve"})
-
- e.RemoveFilteredGroupingPolicy(0, "bob")
-
- testGetRoles(t, e, "alice", []string{})
- testGetRoles(t, e, "bob", []string{})
- testGetRoles(t, e, "eve", []string{"data3_admin"})
- testGetRoles(t, e, "non_exist", []string{})
-
- testGetUsers(t, e, "data1_admin", []string{})
- testGetUsers(t, e, "data2_admin", []string{})
- testGetUsers(t, e, "data3_admin", []string{"eve"})
+ 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
index 3b17637b1..b5b8e9199 100644
--- a/model/assertion.go
+++ b/model/assertion.go
@@ -17,41 +17,183 @@ package model
import (
"errors"
"strings"
+ "sync"
- "github.com/casbin/casbin/rbac"
- "github.com/casbin/casbin/util"
+ "github.com/casbin/casbin/v3/rbac"
)
// Assertion represents an expression in a section of the model.
-// For example: r = sub, obj, act
+// For example: r = sub, obj, act.
type Assertion struct {
- Key string
- Value string
- Tokens []string
- Policy [][]string
- RM rbac.RoleManager
+ 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) buildRoleLinks(rm rbac.RoleManager) {
+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 count < 2 {
- panic(errors.New("the number of \"_\" in role definition should be at least 2"))
+ 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 {
- panic(errors.New("grouping policy elements do not meet role definition"))
+ return errors.New("grouping policy elements do not meet role definition")
+ }
+ if len(rule) > count {
+ rule = rule[:count]
}
- if count == 2 {
- ast.RM.AddLink(rule[0], rule[1])
- } else if count == 3 {
- ast.RM.AddLink(rule[0], rule[1], rule[2])
- } else if count == 4 {
- ast.RM.AddLink(rule[0], rule[1], rule[2], rule[3])
+ 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,
}
- util.LogPrint("Role links for: " + ast.Key)
- ast.RM.PrintRoles()
+ return newAst
}
diff --git a/model/constraint.go b/model/constraint.go
new file mode 100644
index 000000000..b960c1d3e
--- /dev/null
+++ b/model/constraint.go
@@ -0,0 +1,282 @@
+// Copyright 2024 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 model["c"] == nil || len(model["c"]) == 0 {
+ return nil // No constraints to validate
+ }
+
+ // Check if RBAC is enabled
+ if model["g"] == nil || 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
index 5c81d1801..956c94b9d 100644
--- a/model/function.go
+++ b/model/function.go
@@ -14,27 +14,53 @@
package model
-import "github.com/casbin/casbin/util"
+import (
+ "sync"
+
+ "github.com/casbin/casbin/v3/util"
+ "github.com/casbin/govaluate"
+)
// FunctionMap represents the collection of Function.
-type FunctionMap map[string]func(args ...interface{}) (interface{}, error)
+type FunctionMap struct {
+ fns *sync.Map
+}
-// Function represents a function that is used in the matchers, used to get attributes in ABAC.
-type Function func(args ...interface{}) (interface{}, error)
+// [string]govaluate.ExpressionFunction
// AddFunction adds an expression function.
-func (fm FunctionMap) AddFunction(name string, function Function) {
- fm[name] = 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 := make(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 fm
+ return ret
}
diff --git a/model/model.go b/model/model.go
index 55a1c120b..b541e1b84 100644
--- a/model/model.go
+++ b/model/model.go
@@ -15,11 +15,17 @@
package model
import (
+ "container/list"
+ "errors"
+ "fmt"
+ "regexp"
+ "sort"
"strconv"
"strings"
- "github.com/casbin/casbin/config"
- "github.com/casbin/casbin/util"
+ "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.
@@ -28,38 +34,72 @@ 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 = ""
+const defaultSeparator = "::"
+
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
-
- if ast.Value == "" {
- return false
- }
+ ast.PolicyMap = make(map[string]int)
+ ast.FieldIndexMap = make(map[string]int)
if sec == "r" || sec == "p" {
- ast.Tokens = strings.Split(ast.Value, ", ")
+ ast.Tokens = strings.Split(ast.Value, ",")
for i := range ast.Tokens {
- ast.Tokens[i] = key + "_" + ast.Tokens[i]
+ ast.Tokens[i] = key + "_" + strings.TrimSpace(ast.Tokens[i])
}
+ } else if sec == "g" {
+ ast.ParamsTokens = getParamsToken(ast.Value)
+ ast.Tokens = strings.Split(ast.Value, ",")
+ ast.Tokens = ast.Tokens[:len(ast.Tokens)-len(ast.ParamsTokens)]
} else {
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.Replace(strings.Replace(ast.Value, "[", "(", -1), "]", ")", -1)
+ }
+ }
+
_, ok := model[sec]
if !ok {
model[sec] = make(AssertionMap)
@@ -88,42 +128,309 @@ func loadSection(model Model, cfg config.ConfigInterface, sec string) {
}
}
+// 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) {
+func (model Model) LoadModel(path string) error {
cfg, err := config.NewConfig(path)
if err != nil {
- panic(err)
+ return err
}
- loadSection(model, cfg, "r")
- loadSection(model, cfg, "p")
- loadSection(model, cfg, "e")
- loadSection(model, cfg, "m")
-
- loadSection(model, cfg, "g")
+ return model.loadModelFromConfig(cfg)
}
// LoadModelFromText loads the model from the text.
-func (model Model) LoadModelFromText(text string) {
+func (model Model) LoadModelFromText(text string) error {
cfg, err := config.NewConfigFromText(text)
if err != nil {
- panic(err)
+ return err
}
- loadSection(model, cfg, "r")
- loadSection(model, cfg, "p")
- loadSection(model, cfg, "e")
- loadSection(model, cfg, "m")
+ return model.loadModelFromConfig(cfg)
+}
- loadSection(model, cfg, "g")
+// 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() {
- util.LogPrint("Model:")
- for k, v := range model {
- for i, j := range v {
- util.LogPrintf("%s.%s: %s", k, i, j.Value)
+ // 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.Replace(value, tokenPattern, newToken, -1)
+ }
+ s.WriteString(fmt.Sprintf("%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"] {
+ s.WriteString(fmt.Sprintf("%s = %s\n", ptype, model["g"][ptype].Value))
+ }
+ }
+ if _, ok := model["c"]; ok {
+ s.WriteString("[constraint_definition]\n")
+ for ptype := range model["c"] {
+ s.WriteString(fmt.Sprintf("%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
index ebc8bf306..aec970362 100644
--- a/model/model_test.go
+++ b/model/model_test.go
@@ -1,9 +1,161 @@
+// Copyright 2019 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
package model
import (
+ "io/ioutil"
+ "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",
+ },
+ }
)
-func TestModel(t *testing.T) {
- //No tests yet
+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, _ := ioutil.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
index 9d3808ed2..e55bf4105 100644
--- a/model/policy.go
+++ b/model/policy.go
@@ -15,47 +15,113 @@
package model
import (
- "github.com/casbin/casbin/rbac"
- "github.com/casbin/casbin/util"
+ "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(rm rbac.RoleManager) {
- for _, ast := range model["g"] {
- ast.buildRoleLinks(rm)
+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
}
-// PrintPolicy prints the policy to log.
-func (model Model) PrintPolicy() {
- util.LogPrint("Policy:")
- for key, ast := range model["p"] {
- util.LogPrint(key, ": ", ast.Value, ": ", ast.Policy)
+// 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
+}
- for key, ast := range model["g"] {
- util.LogPrint(key, ": ", ast.Value, ": ", ast.Policy)
+// 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 {
- return model[sec][ptype].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 {
+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 {
@@ -72,45 +138,244 @@ func (model Model) GetFilteredPolicy(sec string, ptype string, fieldIndex int, f
}
}
- return res
+ 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 {
- for _, r := range model[sec][ptype].Policy {
- if util.ArrayEquals(rule, r) {
- return true
+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
+ return false, nil
}
// AddPolicy adds a policy rule to the model.
-func (model Model) AddPolicy(sec string, ptype string, rule []string) bool {
- if !model.HasPolicy(sec, ptype, rule) {
- model[sec][ptype].Policy = append(model[sec][ptype].Policy, rule)
- return true
+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
}
- return false
+ 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 {
- for i, r := range model[sec][ptype].Policy {
- if util.ArrayEquals(rule, r) {
- model[sec][ptype].Policy = append(model[sec][ptype].Policy[:i], model[sec][ptype].Policy[i+1:]...)
- return true
+// Deprecated: Using AddPoliciesWithAffected instead.
+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
+ }
+
+ lastIdx := len(ast.Policy) - 1
+ if index != lastIdx {
+ ast.Policy[index] = ast.Policy[lastIdx]
+ lastPolicyKey := strings.Join(ast.Policy[index], DefaultSep)
+ ast.PolicyMap[lastPolicyKey] = index
+ }
+ ast.Policy = ast.Policy[:lastIdx]
+ delete(ast.PolicyMap, key)
+ 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) {
+ _, err := model.GetAssertion(sec, ptype)
+ if err != nil {
+ return nil, err
}
+ var affected [][]string
+ for _, rule := range rules {
+ index, ok := model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)]
+ if !ok {
+ continue
+ }
- return false
+ affected = append(affected, rule)
+ model[sec][ptype].Policy = append(model[sec][ptype].Policy[:index], model[sec][ptype].Policy[index+1:]...)
+ delete(model[sec][ptype].PolicyMap, strings.Join(rule, DefaultSep))
+ for i := index; i < len(model[sec][ptype].Policy); i++ {
+ model[sec][ptype].PolicyMap[strings.Join(model[sec][ptype].Policy[i], DefaultSep)] = i
+ }
+ }
+ 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 {
- tmp := [][]string{}
+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 {
@@ -121,26 +386,74 @@ func (model Model) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int
}
if matched {
- res = true
+ effects = append(effects, rule)
} else {
tmp = append(tmp, rule)
+ model[sec][ptype].PolicyMap[strings.Join(rule, DefaultSep)] = len(tmp) - 1
}
}
- model[sec][ptype].Policy = tmp
- return res
+ 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 {
+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)
- // sort.Strings(values)
- return 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
index b72e169be..82db036dc 100644
--- a/model_b_test.go
+++ b/model_b_test.go
@@ -17,6 +17,8 @@ package casbin
import (
"fmt"
"testing"
+
+ "github.com/casbin/casbin/v3/util"
)
func rawEnforce(sub string, obj string, act string) bool {
@@ -36,134 +38,254 @@ func BenchmarkRaw(b *testing.B) {
}
func BenchmarkBasicModel(b *testing.B) {
- e := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
+ e, _ := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
- e.Enforce("alice", "data1", "read")
+ _, _ = e.Enforce("alice", "data1", "read")
}
}
func BenchmarkRBACModel(b *testing.B) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+ e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
b.ResetTimer()
for i := 0; i < b.N; i++ {
- e.Enforce("alice", "data2", "read")
+ _, _ = 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")
- // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call.
- e.EnableAutoBuildRoleLinks(false)
+ e, _ := NewEnforcer("examples/rbac_model.conf")
+
// 100 roles, 10 resources.
for i := 0; i < 100; i++ {
- e.AddPolicy(fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i / 10), "read")
+ _, 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++ {
- e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i / 10))
+ _, err := e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10))
+ if err != nil {
+ b.Fatal(err)
+ }
}
- e.BuildRoleLinks()
b.ResetTimer()
for i := 0; i < b.N; i++ {
- e.Enforce("user501", "data9", "read")
+ _, _ = e.Enforce("user501", "data9", "read")
}
}
func BenchmarkRBACModelMedium(b *testing.B) {
- e := NewEnforcer("examples/rbac_model.conf")
- // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call.
- e.EnableAutoBuildRoleLinks(false)
+ e, _ := NewEnforcer("examples/rbac_model.conf")
+
// 1000 roles, 100 resources.
+ pPolicies := make([][]string, 0)
for i := 0; i < 1000; i++ {
- e.AddPolicy(fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i / 10), "read")
+ 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++ {
- e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i / 10))
+ 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)
}
- e.BuildRoleLinks()
b.ResetTimer()
for i := 0; i < b.N; i++ {
- e.Enforce("user5001", "data150", "read")
+ _, _ = e.Enforce("user5001", "data99", "read")
}
}
func BenchmarkRBACModelLarge(b *testing.B) {
- e := NewEnforcer("examples/rbac_model.conf")
- // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call.
- e.EnableAutoBuildRoleLinks(false)
+ e, _ := NewEnforcer("examples/rbac_model.conf")
+
// 10000 roles, 1000 resources.
+ pPolicies := make([][]string, 0)
for i := 0; i < 10000; i++ {
- e.AddPolicy(fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i / 10), "read")
+ 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++ {
- e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i / 10))
+ 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)
}
- e.BuildRoleLinks()
b.ResetTimer()
for i := 0; i < b.N; i++ {
- e.Enforce("user50001", "data1500", "read")
+ _, _ = 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")
+ 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")
+ _, _ = e.Enforce("alice", "data1", "read")
}
}
func BenchmarkRBACModelWithDomains(b *testing.B) {
- e := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv")
+ 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")
+ _, _ = e.Enforce("alice", "domain1", "data1", "read")
}
}
func BenchmarkABACModel(b *testing.B) {
- e := NewEnforcer("examples/abac_model.conf")
+ e, _ := NewEnforcer("examples/abac_model.conf")
data1 := newTestResource("data1", "alice")
b.ResetTimer()
for i := 0; i < b.N; i++ {
- e.Enforce("alice", data1, "read")
+ _, _ = 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")
+ 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")
+ _, _ = 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")
+ 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")
+ _, _ = e.Enforce("alice", "data1", "read")
}
}
func BenchmarkPriorityModel(b *testing.B) {
- e := NewEnforcer("examples/priority_model.conf", "examples/priority_policy.csv")
+ 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("alice", "data1", "read")
+ _, _ = e.Enforce("staffUser1001", "/orgs/1/sites/site001", "App001.Module001.Action1001")
}
}
diff --git a/model_test.go b/model_test.go
index 025ad38f8..c3e5432df 100644
--- a/model_test.go
+++ b/model_test.go
@@ -17,40 +17,52 @@ package casbin
import (
"testing"
- "github.com/casbin/casbin/persist/file-adapter"
- "github.com/casbin/casbin/rbac"
+ "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 string, obj interface{}, act string, res bool) {
+func testEnforce(t *testing.T, e *Enforcer, sub interface{}, obj interface{}, act string, res bool) {
t.Helper()
- if e.Enforce(sub, obj, act) != res {
- t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, !res, res)
+ 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 e.Enforce(obj, act) != res {
- t.Errorf("%s, %s: %t, supposed to be %t", obj, act, !res, res)
+ 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 e.Enforce(sub, dom, obj, act) != res {
- t.Errorf("%s, %s, %s, %s: %t, supposed to be %t", sub, dom, obj, act, !res, res)
+ 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 testEnforceSync(t *testing.T, e *SyncedEnforcer, sub string, obj interface{}, act string, res bool) {
- t.Helper()
- if e.Enforce(sub, obj, act) != res {
- t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, !res, 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 TestBasicModel(t *testing.T) {
- e := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
+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)
@@ -63,7 +75,7 @@ func TestBasicModel(t *testing.T) {
}
func TestBasicModelNoPolicy(t *testing.T) {
- e := NewEnforcer("examples/basic_model.conf")
+ e, _ := NewEnforcer("examples/basic_model.conf")
testEnforce(t, e, "alice", "data1", "read", false)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -76,7 +88,7 @@ func TestBasicModelNoPolicy(t *testing.T) {
}
func TestBasicModelWithRoot(t *testing.T) {
- e := NewEnforcer("examples/basic_with_root_model.conf", "examples/basic_policy.csv")
+ 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)
@@ -93,7 +105,7 @@ func TestBasicModelWithRoot(t *testing.T) {
}
func TestBasicModelWithRootNoPolicy(t *testing.T) {
- e := NewEnforcer("examples/basic_with_root_model.conf")
+ e, _ := NewEnforcer("examples/basic_with_root_model.conf")
testEnforce(t, e, "alice", "data1", "read", false)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -110,7 +122,7 @@ func TestBasicModelWithRootNoPolicy(t *testing.T) {
}
func TestBasicModelWithoutUsers(t *testing.T) {
- e := NewEnforcer("examples/basic_without_users_model.conf", "examples/basic_without_users_policy.csv")
+ 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)
@@ -119,7 +131,7 @@ func TestBasicModelWithoutUsers(t *testing.T) {
}
func TestBasicModelWithoutResources(t *testing.T) {
- e := NewEnforcer("examples/basic_without_resources_model.conf", "examples/basic_without_resources_policy.csv")
+ 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)
@@ -128,7 +140,7 @@ func TestBasicModelWithoutResources(t *testing.T) {
}
func TestRBACModel(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+ e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -141,7 +153,7 @@ func TestRBACModel(t *testing.T) {
}
func TestRBACModelWithResourceRoles(t *testing.T) {
- e := NewEnforcer("examples/rbac_with_resource_roles_model.conf", "examples/rbac_with_resource_roles_policy.csv")
+ 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)
@@ -154,7 +166,7 @@ func TestRBACModelWithResourceRoles(t *testing.T) {
}
func TestRBACModelWithDomains(t *testing.T) {
- e := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv")
+ 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)
@@ -167,15 +179,15 @@ func TestRBACModelWithDomains(t *testing.T) {
}
func TestRBACModelWithDomainsAtRuntime(t *testing.T) {
- e := NewEnforcer("examples/rbac_with_domains_model.conf")
+ 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.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")
+ _, _ = 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)
@@ -187,7 +199,7 @@ func TestRBACModelWithDomainsAtRuntime(t *testing.T) {
testDomainEnforce(t, e, "bob", "domain2", "data2", "write", true)
// Remove all policy rules related to domain1 and data1.
- e.RemoveFilteredPolicy(1, "domain1", "data1")
+ _, _ = e.RemoveFilteredPolicy(1, "domain1", "data1")
testDomainEnforce(t, e, "alice", "domain1", "data1", "read", false)
testDomainEnforce(t, e, "alice", "domain1", "data1", "write", false)
@@ -199,7 +211,7 @@ func TestRBACModelWithDomainsAtRuntime(t *testing.T) {
testDomainEnforce(t, e, "bob", "domain2", "data2", "write", true)
// Remove the specified policy rule.
- e.RemovePolicy("admin", "domain2", "data2", "read")
+ _, _ = e.RemovePolicy("admin", "domain2", "data2", "read")
testDomainEnforce(t, e, "alice", "domain1", "data1", "read", false)
testDomainEnforce(t, e, "alice", "domain1", "data1", "write", false)
@@ -213,24 +225,105 @@ func TestRBACModelWithDomainsAtRuntime(t *testing.T) {
func TestRBACModelWithDomainsAtRuntimeMockAdapter(t *testing.T) {
adapter := fileadapter.NewAdapterMock("examples/rbac_with_domains_policy.csv")
- e := NewEnforcer("examples/rbac_with_domains_model.conf", adapter)
+ e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", adapter)
- e.AddPolicy("admin", "domain3", "data1", "read")
- e.AddGroupingPolicy("alice", "admin", "domain3")
+ _, _ = 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")
+ _, _ = 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")
+ _, _ = 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")
+ 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)
@@ -243,18 +336,18 @@ func TestRBACModelWithDeny(t *testing.T) {
}
func TestRBACModelWithOnlyDeny(t *testing.T) {
- e := NewEnforcer("examples/rbac_with_not_deny_model.conf", "examples/rbac_with_deny_policy.csv")
+ 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")
+ 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")
+ _, _ = e.AddGroupingPolicy("bob", "data2_admin", "custom_data")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -268,7 +361,7 @@ func TestRBACModelWithCustomData(t *testing.T) {
// 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")
+ _, _ = e.RemoveGroupingPolicy("bob", "data2_admin", "custom_data")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -280,14 +373,87 @@ func TestRBACModelWithCustomData(t *testing.T) {
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) DeleteLink(name1 string, name2 string, domain ...string) 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
@@ -304,12 +470,43 @@ func (rm *testCustomRoleManager) GetRoles(name string, domain ...string) ([]stri
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, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
e.SetRoleManager(NewRoleManager())
- e.LoadModel()
+ _ = e.LoadModel()
_ = e.LoadPolicy()
testEnforce(t, e, "alice", "data1", "read", true)
@@ -322,36 +519,8 @@ func TestRBACModelWithCustomRoleManager(t *testing.T) {
testEnforce(t, e, "bob", "data2", "write", true)
}
-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 TestKeyMatchModel(t *testing.T) {
- e := NewEnforcer("examples/keymatch_model.conf", "examples/keymatch_policy.csv")
+ 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)
@@ -377,7 +546,7 @@ func TestKeyMatchModel(t *testing.T) {
}
func TestKeyMatch2Model(t *testing.T) {
- e := NewEnforcer("examples/keymatch2_model.conf", "examples/keymatch2_policy.csv")
+ 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)
@@ -385,28 +554,34 @@ func TestKeyMatch2Model(t *testing.T) {
testEnforce(t, e, "alice", "/alice_data2/myid/using/res_id", "GET", true)
}
-func KeyMatchCustom(args ...interface{}) (interface{}, error) {
- match := false
- if args[0].(string) == "/alice_data2/myid/using/res_id" && args[1].(string) == "/alice_data/:resource" {
- match = true
- }
- if args[0].(string) == "/alice_data2/myid/using/res_id" && args[1].(string) == "/alice_data2/:id/using/:resId" {
- match = 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
}
- return match, nil
+}
+
+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, _ := NewEnforcer("examples/keymatch_custom_model.conf", "examples/keymatch2_policy.csv")
- e.AddFunction("keyMatchCustom", KeyMatchCustom)
+ 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")
+ 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)
@@ -429,8 +604,27 @@ func TestIPMatchModel(t *testing.T) {
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")
+ e, _ := NewEnforcer("examples/priority_model.conf", "examples/priority_policy.csv")
testEnforce(t, e, "alice", "data1", "read", true)
testEnforce(t, e, "alice", "data1", "write", false)
@@ -443,7 +637,152 @@ func TestPriorityModel(t *testing.T) {
}
func TestPriorityModelIndeterminate(t *testing.T) {
- e := NewEnforcer("examples/priority_model.conf", "examples/priority_indeterminate_policy.csv")
+ 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 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 000000000..0dcb8b6f7
--- /dev/null
+++ b/orbac_test.go
@@ -0,0 +1,80 @@
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..4b0019493
--- /dev/null
+++ b/pbac_test.go
@@ -0,0 +1,66 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 0d994b98f..800e7570b 100644
--- a/persist/adapter.go
+++ b/persist/adapter.go
@@ -15,26 +15,49 @@
package persist
import (
+ "encoding/csv"
"strings"
- "github.com/casbin/casbin/model"
+ "github.com/casbin/casbin/v3/model"
)
// LoadPolicyLine loads a text line as a policy rule to model.
-func LoadPolicyLine(line string, model model.Model) {
- if line == "" {
- return
+func LoadPolicyLine(line string, m model.Model) error {
+ if line == "" || strings.HasPrefix(line, "#") {
+ return nil
}
- if strings.HasPrefix(line, "#") {
- return
+ r := csv.NewReader(strings.NewReader(line))
+ r.Comma = ','
+ r.Comment = '#'
+ r.TrimLeadingSpace = true
+
+ tokens, err := r.Read()
+ if err != nil {
+ return err
}
- tokens := strings.Split(line, ", ")
+ return LoadPolicyArray(tokens, m)
+}
- key := tokens[0]
+// LoadPolicyArray loads a policy rule to model.
+func LoadPolicyArray(rule []string, m model.Model) error {
+ key := rule[0]
sec := key[:1]
- model[sec][key].Policy = append(model[sec][key].Policy, tokens[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.
diff --git a/persist/adapter_context.go b/persist/adapter_context.go
new file mode 100644
index 000000000..717524dfd
--- /dev/null
+++ b/persist/adapter_context.go
@@ -0,0 +1,39 @@
+// Copyright 2023 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index fab51e725..f4be4a6bd 100644
--- a/persist/adapter_filtered.go
+++ b/persist/adapter_filtered.go
@@ -15,7 +15,7 @@
package persist
import (
- "github.com/casbin/casbin/model"
+ "github.com/casbin/casbin/v3/model"
)
// FilteredAdapter is the interface for Casbin adapters supporting filtered policies.
diff --git a/persist/adapter_filtered_context.go b/persist/adapter_filtered_context.go
new file mode 100644
index 000000000..bd348e3cf
--- /dev/null
+++ b/persist/adapter_filtered_context.go
@@ -0,0 +1,31 @@
+// Copyright 2024 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..56ec415fe
--- /dev/null
+++ b/persist/batch_adapter.go
@@ -0,0 +1,26 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..741c184d6
--- /dev/null
+++ b/persist/batch_adapter_context.go
@@ -0,0 +1,29 @@
+// Copyright 2024 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..08447b83c
--- /dev/null
+++ b/persist/cache/cache.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..816e12dcc
--- /dev/null
+++ b/persist/cache/cache_sync.go
@@ -0,0 +1,86 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..9108e7d64
--- /dev/null
+++ b/persist/cache/default-cache.go
@@ -0,0 +1,69 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..ceaed8385
--- /dev/null
+++ b/persist/dispatcher.go
@@ -0,0 +1,33 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 9b9b492cd..454b2d13c 100644
--- a/persist/file-adapter/adapter.go
+++ b/persist/file-adapter/adapter.go
@@ -21,9 +21,9 @@ import (
"os"
"strings"
- "github.com/casbin/casbin/model"
- "github.com/casbin/casbin/persist"
- "github.com/casbin/casbin/util"
+ "github.com/casbin/casbin/v3/model"
+ "github.com/casbin/casbin/v3/persist"
+ "github.com/casbin/casbin/v3/util"
)
// Adapter is the file adapter for Casbin.
@@ -32,11 +32,21 @@ 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 {
- a := Adapter{}
- a.filePath = filePath
- return &a
+ return &Adapter{filePath: filePath}
}
// LoadPolicy loads all policy rules from the storage.
@@ -45,8 +55,7 @@ func (a *Adapter) LoadPolicy(model model.Model) error {
return errors.New("invalid file path, file path cannot be empty")
}
- err := a.loadPolicyFile(model, persist.LoadPolicyLine)
- return err
+ return a.loadPolicyFile(model, persist.LoadPolicyLine)
}
// SavePolicy saves all policy rules to the storage.
@@ -73,11 +82,10 @@ func (a *Adapter) SavePolicy(model model.Model) error {
}
}
- err := a.savePolicyFile(strings.TrimRight(tmp.String(), "\n"))
- return err
+ return a.savePolicyFile(strings.TrimRight(tmp.String(), "\n"))
}
-func (a *Adapter) loadPolicyFile(model model.Model, handler func(string, model.Model)) error {
+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
@@ -87,7 +95,10 @@ func (a *Adapter) loadPolicyFile(model model.Model, handler func(string, model.M
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
- handler(line, model)
+ err = handler(line, model)
+ if err != nil {
+ return err
+ }
}
return scanner.Err()
}
@@ -98,10 +109,18 @@ func (a *Adapter) savePolicyFile(text string) error {
return err
}
w := bufio.NewWriter(f)
- w.WriteString(text)
- w.Flush()
- f.Close()
- return nil
+
+ _, 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.
@@ -109,11 +128,21 @@ 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 000000000..500b618c8
--- /dev/null
+++ b/persist/file-adapter/adapter_context.go
@@ -0,0 +1,117 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 23c933e35..bf033f98b 100644
--- a/persist/file-adapter/adapter_filtered.go
+++ b/persist/file-adapter/adapter_filtered.go
@@ -20,8 +20,8 @@ import (
"os"
"strings"
- "github.com/casbin/casbin/model"
- "github.com/casbin/casbin/persist"
+ "github.com/casbin/casbin/v3/model"
+ "github.com/casbin/casbin/v3/persist"
)
// FilteredAdapter is the filtered file adapter for Casbin. It can load policy
@@ -34,23 +34,30 @@ type FilteredAdapter struct {
// 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
+ 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)
}
-// LoadPolicy loads all policy rules from the storage.
+// 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)
@@ -70,7 +77,7 @@ func (a *FilteredAdapter) LoadFilteredPolicy(model model.Model, filter interface
return err
}
-func (a *FilteredAdapter) loadFilteredPolicyFile(model model.Model, filter *Filter, handler func(string, model.Model)) error {
+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
@@ -85,7 +92,10 @@ func (a *FilteredAdapter) loadFilteredPolicyFile(model model.Model, filter *Filt
continue
}
- handler(line, model)
+ err = handler(line, model)
+ if err != nil {
+ return err
+ }
}
return scanner.Err()
}
@@ -97,7 +107,7 @@ func (a *FilteredAdapter) IsFiltered() bool {
// SavePolicy saves all policy rules to the storage.
func (a *FilteredAdapter) SavePolicy(model model.Model) error {
- if a.filtered == true {
+ if a.filtered {
return errors.New("cannot save a filtered policy")
}
return a.Adapter.SavePolicy(model)
@@ -117,6 +127,16 @@ func filterLine(line string, filter *Filter) bool {
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)
}
diff --git a/persist/file-adapter/adapter_filtered_context.go b/persist/file-adapter/adapter_filtered_context.go
new file mode 100644
index 000000000..e0f3319f9
--- /dev/null
+++ b/persist/file-adapter/adapter_filtered_context.go
@@ -0,0 +1,57 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 2f5ec7c76..6f1ddadcb 100644
--- a/persist/file-adapter/adapter_mock.go
+++ b/persist/file-adapter/adapter_mock.go
@@ -21,8 +21,8 @@ import (
"os"
"strings"
- "github.com/casbin/casbin/model"
- "github.com/casbin/casbin/persist"
+ "github.com/casbin/casbin/v3/model"
+ "github.com/casbin/casbin/v3/persist"
)
// AdapterMock is the file adapter for Casbin.
@@ -50,7 +50,7 @@ func (a *AdapterMock) SavePolicy(model model.Model) error {
return nil
}
-func (a *AdapterMock) loadPolicyFile(model model.Model, handler func(string, model.Model)) error {
+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
@@ -61,19 +61,24 @@ func (a *AdapterMock) loadPolicyFile(model model.Model, handler func(string, mod
for {
line, err := buf.ReadString('\n')
line = strings.TrimSpace(line)
- handler(line, model)
+ 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 != "" {
@@ -87,11 +92,30 @@ 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
index f3e44229a..84b38ea81 100644
--- a/persist/persist_test.go
+++ b/persist/persist_test.go
@@ -1,9 +1,53 @@
-package persist
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+package persist_test
import (
"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
+ // 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")
}
diff --git a/persist/string-adapter/adapter.go b/persist/string-adapter/adapter.go
new file mode 100644
index 000000000..c9655acbe
--- /dev/null
+++ b/persist/string-adapter/adapter.go
@@ -0,0 +1,92 @@
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+package stringadapter
+
+import (
+ "bytes"
+ "errors"
+ "strings"
+
+ "github.com/casbin/casbin/v3/model"
+ "github.com/casbin/casbin/v3/persist"
+ "github.com/casbin/casbin/v3/util"
+)
+
+// 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 {
+ if str == "" {
+ continue
+ }
+ _ = persist.LoadPolicyLine(str, model)
+ }
+
+ 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 {
+ tmp.WriteString(ptype + ", ")
+ tmp.WriteString(util.ArrayToString(rule))
+ tmp.WriteString("\n")
+ }
+ }
+
+ for ptype, ast := range model["g"] {
+ for _, rule := range ast.Policy {
+ tmp.WriteString(ptype + ", ")
+ tmp.WriteString(util.ArrayToString(rule))
+ 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 000000000..cfc22b64a
--- /dev/null
+++ b/persist/string-adapter/adapter_context.go
@@ -0,0 +1,75 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..5e8727b53
--- /dev/null
+++ b/persist/string-adapter/adapter_test.go
@@ -0,0 +1,101 @@
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+package stringadapter
+
+import (
+ "testing"
+
+ "github.com/casbin/casbin/v3"
+ "github.com/casbin/casbin/v3/model"
+)
+
+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")
+ }
+}
+
+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 000000000..0b69e4754
--- /dev/null
+++ b/persist/transaction.go
@@ -0,0 +1,58 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..fe9204afd
--- /dev/null
+++ b/persist/update_adapter.go
@@ -0,0 +1,27 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..55b8ba9df
--- /dev/null
+++ b/persist/update_adapter_context.go
@@ -0,0 +1,30 @@
+// Copyright 2024 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 089fbc0f2..0d843606b 100644
--- a/persist/watcher.go
+++ b/persist/watcher.go
@@ -24,4 +24,6 @@ type Watcher interface {
// 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 000000000..269ce5f45
--- /dev/null
+++ b/persist/watcher_ex.go
@@ -0,0 +1,40 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..694123c46
--- /dev/null
+++ b/persist/watcher_update.go
@@ -0,0 +1,26 @@
+// Copyright 2020 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..dcaa37f76
--- /dev/null
+++ b/rbac/context_role_manager.go
@@ -0,0 +1,46 @@
+// Copyright 2023 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index da80ccbb8..d6e9d164e 100644
--- a/rbac/default-role-manager/role_manager.go
+++ b/rbac/default-role-manager/role_manager.go
@@ -18,223 +18,1233 @@ import (
"errors"
"sync"
- "github.com/casbin/casbin/rbac"
- "github.com/casbin/casbin/util"
+ "github.com/casbin/casbin/v3/rbac"
+ "github.com/casbin/casbin/v3/util"
)
-type RoleManager struct {
- allRoles *sync.Map
- maxHierarchyLevel int
+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
}
-// NewRoleManager is the constructor for creating an instance of the
+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 NewRoleManager(maxHierarchyLevel int) rbac.RoleManager {
- rm := RoleManager{}
- rm.allRoles = &sync.Map{}
+func NewRoleManagerImpl(maxHierarchyLevel int) *RoleManagerImpl {
+ rm := RoleManagerImpl{}
+ _ = rm.Clear() // init allRoles and matchingFuncCache
rm.maxHierarchyLevel = maxHierarchyLevel
return &rm
}
-func (rm *RoleManager) hasRole(name string) bool {
- _, ok := rm.allRoles.Load(name)
- return ok
+// 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
}
-func (rm *RoleManager) createRole(name string) *Role {
- role, _ := rm.allRoles.LoadOrStore(name, newRole(name))
- return role.(*Role)
+// 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 *RoleManager) Clear() error {
+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.
-// domain is a prefix to the roles.
-func (rm *RoleManager) AddLink(name1 string, name2 string, domain ...string) error {
- if len(domain) == 1 {
- name1 = domain[0] + "::" + name1
- name2 = domain[0] + "::" + name2
- } else if len(domain) > 1 {
- return errors.New("error: domain should be 1 parameter")
- }
-
- role1 := rm.createRole(name1)
- role2 := rm.createRole(name2)
- role1.addRole(role2)
+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.
-// domain is a prefix to the roles.
-func (rm *RoleManager) DeleteLink(name1 string, name2 string, domain ...string) error {
- if len(domain) == 1 {
- name1 = domain[0] + "::" + name1
- name2 = domain[0] + "::" + name2
- } else if len(domain) > 1 {
- return errors.New("error: domain should be 1 parameter")
+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)
}
- if !rm.hasRole(name1) || !rm.hasRole(name2) {
- return errors.New("error: name1 or name2 does not exist")
+ 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
+ })
}
- role1 := rm.createRole(name1)
- role2 := rm.createRole(name2)
- role1.deleteRole(role2)
+ 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
}
-// HasLink determines whether role: name1 inherits role: name2.
-// domain is a prefix to the roles.
-func (rm *RoleManager) HasLink(name1 string, name2 string, domain ...string) (bool, error) {
- if len(domain) == 1 {
- name1 = domain[0] + "::" + name1
- name2 = domain[0] + "::" + name2
- } else if len(domain) > 1 {
- return false, errors.New("error: domain should be 1 parameter")
+// 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
}
+}
- if name1 == name2 {
- return true, nil
+func (dm *DomainManager) Match(str string, pattern string) bool {
+ if str == pattern {
+ return true
}
- if !rm.hasRole(name1) || !rm.hasRole(name2) {
- return false, nil
+ if dm.domainMatchingFunc != nil {
+ return dm.domainMatchingFunc(str, pattern)
+ } else {
+ return false
}
+}
- role1 := rm.createRole(name1)
- return role1.hasRole(name2, rm.maxHierarchyLevel), nil
+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.
-// domain is a prefix to the roles.
-func (rm *RoleManager) GetRoles(name string, domain ...string) ([]string, error) {
- if len(domain) == 1 {
- name = domain[0] + "::" + name
- } else if len(domain) > 1 {
- return nil, errors.New("error: domain should be 1 parameter")
+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...)
+}
- if !rm.hasRole(name) {
- return nil, errors.New("error: name does not exist")
+// 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...)
+}
- roles := rm.createRole(name).getRoles()
- if len(domain) == 1 {
- for i := range roles {
- roles[i] = roles[i][len(domain[0])+2:]
- }
+// 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
}
- return roles, nil
+ rm := dm.getRoleManager(domain, false)
+ return rm.GetImplicitRoles(name, domains...)
}
-// GetUsers gets the users that inherits a subject.
-// domain is an unreferenced parameter here, may be used in other implementations.
-func (rm *RoleManager) GetUsers(name string, domain ...string) ([]string, error) {
- if !rm.hasRole(name) {
- return nil, errors.New("error: name does not exist")
+// 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...)
+}
- names := []string{}
- rm.allRoles.Range(func(_, value interface{}) bool {
- role := value.(*Role)
- if role.hasDirectRole(name) {
- names = append(names, role.name)
+// 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 names, nil
+ return domains, nil
}
-// PrintRoles prints all the roles to log.
-func (rm *RoleManager) PrintRoles() error {
- rm.allRoles.Range(func(_, value interface{}) bool {
- util.LogPrint(value.(*Role).toString())
+// 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
}
-// Role represents the data structure for a role in RBAC.
-type Role struct {
- name string
- roles []*Role
+// DeleteDomain deletes the specified domain from DomainManager.
+func (dm *DomainManager) DeleteDomain(domain string) error {
+ dm.rmMap.Delete(domain)
+ return nil
}
-func newRole(name string) *Role {
- r := Role{}
- r.name = name
- return &r
+type RoleManager struct {
+ *DomainManager
}
-func (r *Role) addRole(role *Role) {
- for _, rr := range r.roles {
- if rr.name == role.name {
- return
+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)
+ })
}
- r.roles = append(r.roles, role)
+ return crm.hasLinkHelper(targetName, nextRoles, level-1)
}
-func (r *Role) deleteRole(role *Role) {
- for i, rr := range r.roles {
- if rr.name == role.name {
- r.roles = append(r.roles[:i], r.roles[i+1:]...)
- return
+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 (r *Role) hasRole(name string, hierarchyLevel int) bool {
- if r.name == name {
- return true
+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 hierarchyLevel <= 0 {
- return false
+ 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)
- for _, role := range r.roles {
- if role.hasRole(name, hierarchyLevel-1) {
+ 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
}
- return false
+
+ 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)
}
-func (r *Role) hasDirectRole(name string) bool {
- for _, role := range r.roles {
- if role.name == name {
+// 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
}
- return 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...)
}
-func (r *Role) toString() string {
- names := ""
- for i, role := range r.roles {
- if i == 0 {
- names += role.name
- } else {
- names += ", " + role.name
+// 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 r.name + " < " + names
+ return rm
}
-func (r *Role) getRoles() []string {
- names := []string{}
- for _, role := range r.roles {
- names = append(names, role.name)
+// 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
}
- return names
+ 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
index 9a864f43c..679d5458b 100644
--- a/rbac/default-role-manager/role_manager_test.go
+++ b/rbac/default-role-manager/role_manager_test.go
@@ -15,17 +15,19 @@
package defaultrolemanager
import (
- "log"
+ "fmt"
+ "sync"
+ "sync/atomic"
"testing"
- "github.com/casbin/casbin/rbac"
- "github.com/casbin/casbin/util"
+ "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)
- log.Printf("%s, %s: %t", name1, name2, myRes)
+ t.Logf("%s, %s: %t", name1, name2, myRes)
if myRes != res {
t.Errorf("%s < %s: %t, supposed to be %t", name1, name2, !res, res)
@@ -35,7 +37,7 @@ func testRole(t *testing.T, rm rbac.RoleManager, name1 string, name2 string, 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)
- log.Printf("%s :: %s, %s: %t", domain, name1, name2, myRes)
+ 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)
@@ -45,21 +47,40 @@ func testDomainRole(t *testing.T, rm rbac.RoleManager, name1 string, name2 strin
func testPrintRoles(t *testing.T, rm rbac.RoleManager, name string, res []string) {
t.Helper()
myRes, _ := rm.GetRoles(name)
- log.Printf("%s: %s", name, myRes)
+ t.Logf("%s: %s", name, myRes)
- if !util.ArrayEquals(myRes, res) {
+ 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")
+ _ = 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
@@ -89,8 +110,8 @@ func TestRole(t *testing.T) {
testPrintRoles(t, rm, "g2", []string{})
testPrintRoles(t, rm, "g3", []string{})
- rm.DeleteLink("g1", "g3")
- rm.DeleteLink("u4", "g2")
+ _ = rm.DeleteLink("g1", "g3")
+ _ = rm.DeleteLink("u4", "g2")
// Current role inheritance tree after deleting the links:
// g3 g2
@@ -119,16 +140,36 @@ func TestRole(t *testing.T) {
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")
+ _ = 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
@@ -157,8 +198,8 @@ func TestDomainRole(t *testing.T) {
testDomainRole(t, rm, "u4", "admin", "domain1", true)
testDomainRole(t, rm, "u4", "admin", "domain2", true)
- rm.DeleteLink("g1", "admin", "domain1")
- rm.DeleteLink("u4", "admin", "domain2")
+ _ = rm.DeleteLink("g1", "admin", "domain1")
+ _ = rm.DeleteLink("u4", "admin", "domain2")
// Current role inheritance tree after deleting the links:
// domain1:admin domain2:admin
@@ -190,12 +231,12 @@ func TestDomainRole(t *testing.T) {
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")
+ _ = 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
@@ -204,7 +245,7 @@ func TestClear(t *testing.T) {
// / \
// u1 u2
- rm.Clear()
+ _ = rm.Clear()
// All data is cleared.
// No role inheritance now.
@@ -222,3 +263,171 @@ func TestClear(t *testing.T) {
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
index b853bc931..3e6113312 100644
--- a/rbac/role_manager.go
+++ b/rbac/role_manager.go
@@ -14,6 +14,10 @@
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.
@@ -21,6 +25,8 @@ type RoleManager interface {
// 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
@@ -33,6 +39,42 @@ type RoleManager interface {
// 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
index 4a78b6fd1..c1ca4f7a3 100644
--- a/rbac_api.go
+++ b/rbac_api.go
@@ -14,22 +14,43 @@
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) []string {
- res, _ := e.model["g"]["g"].RM.GetRoles(name)
- return res
+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) []string {
- res, _ := e.model["g"]["g"].RM.GetUsers(name)
- return res
+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) bool {
- roles := e.GetRolesForUser(name)
-
+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 {
@@ -38,90 +59,672 @@ func (e *Enforcer) HasRoleForUser(name string, role string) bool {
}
}
- return hasRole
+ 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) bool {
- return e.AddGroupingPolicy(user, role)
+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) bool {
- return e.RemoveGroupingPolicy(user, role)
+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) bool {
- return e.RemoveFilteredGroupingPolicy(0, user)
+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 {
- return e.RemoveFilteredGroupingPolicy(0, user)
+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.
-func (e *Enforcer) DeleteRole(role string) {
- e.RemoveFilteredGroupingPolicy(1, role)
- e.RemoveFilteredPolicy(0, 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 {
+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 {
- params := make([]interface{}, 0, len(permission)+1)
+func (e *Enforcer) AddPermissionForUser(user string, permission ...string) (bool, error) {
+ return e.AddPolicy(util.JoinSlice(user, permission...))
+}
- params = append(params, user)
- for _, perm := range permission {
- params = append(params, perm)
+// 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.AddPolicy(params...)
+ 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 {
- params := make([]interface{}, 0, len(permission)+1)
-
- params = append(params, user)
- for _, perm := range permission {
- params = append(params, perm)
- }
-
- return e.RemovePolicy(params...)
+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 {
- return e.RemoveFilteredPolicy(0, user)
+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) [][]string {
- return e.GetFilteredPolicy(0, user)
+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("p", 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 {
- params := make([]interface{}, 0, len(permission)+1)
+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
- params = append(params, user)
- for _, perm := range permission {
- params = append(params, perm)
+ for rm := range e.rmMap {
+ roles, err := e.GetNamedImplicitRolesForUser(rm, name, domain...)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, roles...)
}
- return e.HasPolicy(params...)
+ 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 000000000..adcf8e2ed
--- /dev/null
+++ b/rbac_api_context.go
@@ -0,0 +1,131 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+// 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
index 119ce9f8b..8767bb671 100644
--- a/rbac_api_synced.go
+++ b/rbac_api_synced.go
@@ -15,68 +15,77 @@
package casbin
// GetRolesForUser gets the roles that a user has.
-func (e *SyncedEnforcer) GetRolesForUser(name string) []string {
- e.m.Lock()
- defer e.m.Unlock()
- return e.Enforcer.GetRolesForUser(name)
+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) []string {
- e.m.Lock()
- defer e.m.Unlock()
- return e.Enforcer.GetUsersForRole(name)
+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) bool {
- e.m.Lock()
- defer e.m.Unlock()
- return e.Enforcer.HasRoleForUser(name, 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) bool {
+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.AddRoleForUser(user, role)
+ 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) bool {
+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)
+ 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) bool {
+func (e *SyncedEnforcer) DeleteRolesForUser(user string, domain ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
- return e.Enforcer.DeleteRolesForUser(user)
+ 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 {
+func (e *SyncedEnforcer) DeleteUser(user string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.DeleteUser(user)
}
// DeleteRole deletes a role.
-func (e *SyncedEnforcer) DeleteRole(role string) {
+// 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()
- e.Enforcer.DeleteRole(role)
+ 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 {
+func (e *SyncedEnforcer) DeletePermission(permission ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.DeletePermission(permission...)
@@ -84,15 +93,23 @@ func (e *SyncedEnforcer) DeletePermission(permission ...string) bool {
// 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 {
+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 {
+func (e *SyncedEnforcer) DeletePermissionForUser(user string, permission ...string) (bool, error) {
e.m.Lock()
defer e.m.Unlock()
return e.Enforcer.DeletePermissionForUser(user, permission...)
@@ -100,22 +117,102 @@ func (e *SyncedEnforcer) DeletePermissionForUser(user string, permission ...stri
// 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 {
+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) [][]string {
- e.m.Lock()
- defer e.m.Unlock()
- return e.Enforcer.GetPermissionsForUser(user)
+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 {
+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.HasPermissionForUser(user, permission...)
+ 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
index fb45e37db..6d88eded1 100644
--- a/rbac_api_test.go
+++ b/rbac_api_test.go
@@ -15,36 +15,55 @@
package casbin
import (
+ "fmt"
"log"
+ "sort"
"testing"
- "github.com/casbin/casbin/util"
+ "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, name string, res []string) {
+func testGetRoles(t *testing.T, e *Enforcer, res []string, name string, domain ...string) {
t.Helper()
- myRes := e.GetRolesForUser(name)
- log.Print("Roles for ", name, ": ", myRes)
+ 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, name string, res []string) {
+func testGetUsers(t *testing.T, e *Enforcer, res []string, name string, domain ...string) {
t.Helper()
- myRes := e.GetUsersForRole(name)
- log.Print("Users for ", name, ": ", myRes)
+ 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) {
+func testHasRole(t *testing.T, e *Enforcer, name string, role string, res bool, domain ...string) {
t.Helper()
- myRes := e.HasRoleForUser(name, role)
- log.Print(name, " has role ", role, ": ", myRes)
+ 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)
@@ -52,44 +71,44 @@ func testHasRole(t *testing.T, e *Enforcer, name string, role string, res bool)
}
func TestRoleAPI(t *testing.T) {
- e := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+ e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
- testGetRoles(t, e, "alice", []string{"data2_admin"})
- testGetRoles(t, e, "bob", []string{})
- testGetRoles(t, e, "data2_admin", []string{})
- testGetRoles(t, e, "non_exist", []string{})
+ 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")
+ _, _ = e.AddRoleForUser("alice", "data1_admin")
- testGetRoles(t, e, "alice", []string{"data1_admin", "data2_admin"})
- testGetRoles(t, e, "bob", []string{})
- testGetRoles(t, e, "data2_admin", []string{})
+ 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")
+ _, _ = e.DeleteRoleForUser("alice", "data1_admin")
- testGetRoles(t, e, "alice", []string{"data2_admin"})
- testGetRoles(t, e, "bob", []string{})
- testGetRoles(t, e, "data2_admin", []string{})
+ testGetRoles(t, e, []string{"data2_admin"}, "alice")
+ testGetRoles(t, e, []string{}, "bob")
+ testGetRoles(t, e, []string{}, "data2_admin")
- e.DeleteRolesForUser("alice")
+ _, _ = e.DeleteRolesForUser("alice")
- testGetRoles(t, e, "alice", []string{})
- testGetRoles(t, e, "bob", []string{})
- testGetRoles(t, e, "data2_admin", []string{})
+ testGetRoles(t, e, []string{}, "alice")
+ testGetRoles(t, e, []string{}, "bob")
+ testGetRoles(t, e, []string{}, "data2_admin")
- e.AddRoleForUser("alice", "data1_admin")
- e.DeleteUser("alice")
+ _, _ = e.AddRoleForUser("alice", "data1_admin")
+ _, _ = e.DeleteUser("alice")
- testGetRoles(t, e, "alice", []string{})
- testGetRoles(t, e, "bob", []string{})
- testGetRoles(t, e, "data2_admin", []string{})
+ testGetRoles(t, e, []string{}, "alice")
+ testGetRoles(t, e, []string{}, "bob")
+ testGetRoles(t, e, []string{}, "data2_admin")
- e.AddRoleForUser("alice", "data2_admin")
+ _, _ = e.AddRoleForUser("alice", "data2_admin")
- testEnforce(t, e, "alice", "data1", "read", true)
+ 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)
@@ -98,9 +117,9 @@ func TestRoleAPI(t *testing.T) {
testEnforce(t, e, "bob", "data2", "read", false)
testEnforce(t, e, "bob", "data2", "write", true)
- e.DeleteRole("data2_admin")
+ _, _ = e.DeleteRole("data2_admin")
- testEnforce(t, e, "alice", "data1", "read", true)
+ 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)
@@ -110,10 +129,75 @@ func TestRoleAPI(t *testing.T) {
testEnforce(t, e, "bob", "data2", "write", true)
}
-func testGetPermissions(t *testing.T, e *Enforcer, name string, res [][]string) {
+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 := e.GetPermissionsForUser(name)
- log.Print("Permissions for ", name, ": ", myRes)
+ 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)
@@ -122,16 +206,33 @@ func testGetPermissions(t *testing.T, e *Enforcer, name string, res [][]string)
func testHasPermission(t *testing.T, e *Enforcer, name string, permission []string, res bool) {
t.Helper()
- myRes := e.HasPermissionForUser(name, permission...)
- log.Print(name, " has permission ", util.ArrayToString(permission), ": ", myRes)
+ 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")
+ 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)
@@ -146,31 +247,637 @@ func TestPermissionAPI(t *testing.T) {
testHasPermission(t, e, "bob", []string{"read"}, false)
testHasPermission(t, e, "bob", []string{"write"}, true)
- e.DeletePermission("read")
+ _, _ = 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")
+ _, _ = 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.DeletePermissionForUser("bob", "read")
+ _, _ = 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")
+ _, _ = 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
index d6af0d6d6..2f4bc0f76 100644
--- a/rbac_api_with_domains.go
+++ b/rbac_api_with_domains.go
@@ -14,25 +14,188 @@
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 {
- res, _ := e.model["g"]["g"].RM.GetRoles(name, domain)
- return res
+ 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 {
- return e.GetFilteredPolicy(0, user, domain)
+ 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 {
+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 {
+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 000000000..4e9b00a56
--- /dev/null
+++ b/rbac_api_with_domains_context.go
@@ -0,0 +1,113 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..02fadb148
--- /dev/null
+++ b/rbac_api_with_domains_synced.go
@@ -0,0 +1,68 @@
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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
index 5c5ad8d0b..956cfc4b6 100644
--- a/rbac_api_with_domains_test.go
+++ b/rbac_api_with_domains_test.go
@@ -15,69 +15,411 @@
package casbin
import (
- "log"
+ "sort"
"testing"
- "github.com/casbin/casbin/util"
+ "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)
- log.Print("Roles for ", name, " under ", domain, " : ", myRes)
+ 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)
+ 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")
+ 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")
+ _, _ = 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)
- log.Print("Permissions for ", name, " under ", domain, " : ", myRes)
+ 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)
+ 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")
+ e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv")
- testGetPermissionsInDomain(t, e, "alice", "domain1", [][]string{})
+ 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{})
+ 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/role_manager_b_test.go b/role_manager_b_test.go
new file mode 100644
index 000000000..b6a866740
--- /dev/null
+++ b/role_manager_b_test.go
@@ -0,0 +1,195 @@
+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 000000000..5c9749c6f
--- /dev/null
+++ b/syntax_test.go
@@ -0,0 +1,34 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..3aac9e305
--- /dev/null
+++ b/transaction.go
@@ -0,0 +1,435 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+package casbin
+
+import (
+ "context"
+ "errors"
+ "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.
+}
+
+// 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...)
+}
+
+// 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
+}
+
+// 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) {
+ tx.mutex.Lock()
+ defer tx.mutex.Unlock()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return false, err
+ }
+
+ rule := tx.buildRuleFromParams(params...)
+
+ // Check if policy already exists in the buffered model.
+ bufferedModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+ if err != nil {
+ return false, err
+ }
+
+ hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
+ if hasPolicy || err != nil {
+ return false, err
+ }
+
+ // Add operation to buffer.
+ op := persist.PolicyOperation{
+ Type: persist.OperationAdd,
+ Section: "p",
+ PolicyType: ptype,
+ Rules: [][]string{rule},
+ }
+ tx.buffer.AddOperation(op)
+
+ return true, nil
+}
+
+// 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) {
+ tx.mutex.Lock()
+ defer tx.mutex.Unlock()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return false, err
+ }
+
+ if len(rules) == 0 {
+ return false, nil
+ }
+
+ // Check if any policies already exist in the buffered model.
+ bufferedModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+ if err != nil {
+ return false, err
+ }
+
+ var validRules [][]string
+ for _, rule := range rules {
+ hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
+ if err != nil {
+ return false, err
+ }
+ if !hasPolicy {
+ validRules = append(validRules, rule)
+ }
+ }
+
+ if len(validRules) == 0 {
+ return false, nil
+ }
+
+ // Add operation to buffer.
+ op := persist.PolicyOperation{
+ Type: persist.OperationAdd,
+ Section: "p",
+ PolicyType: ptype,
+ Rules: validRules,
+ }
+ tx.buffer.AddOperation(op)
+
+ return true, nil
+}
+
+// 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) {
+ tx.mutex.Lock()
+ defer tx.mutex.Unlock()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return false, err
+ }
+
+ rule := tx.buildRuleFromParams(params...)
+
+ // Check if policy exists in the buffered model.
+ bufferedModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+ if err != nil {
+ return false, err
+ }
+
+ hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
+ if !hasPolicy || err != nil {
+ return false, err
+ }
+
+ // Add operation to buffer.
+ op := persist.PolicyOperation{
+ Type: persist.OperationRemove,
+ Section: "p",
+ PolicyType: ptype,
+ Rules: [][]string{rule},
+ }
+ tx.buffer.AddOperation(op)
+
+ return true, nil
+}
+
+// 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) {
+ tx.mutex.Lock()
+ defer tx.mutex.Unlock()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return false, err
+ }
+
+ if len(rules) == 0 {
+ return false, nil
+ }
+
+ // Check if policies exist in the buffered model.
+ bufferedModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+ if err != nil {
+ return false, err
+ }
+
+ var validRules [][]string
+ for _, rule := range rules {
+ hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
+ if err != nil {
+ return false, err
+ }
+ if hasPolicy {
+ validRules = append(validRules, rule)
+ }
+ }
+
+ if len(validRules) == 0 {
+ return false, nil
+ }
+
+ // Add operation to buffer.
+ op := persist.PolicyOperation{
+ Type: persist.OperationRemove,
+ Section: "p",
+ PolicyType: ptype,
+ Rules: validRules,
+ }
+ tx.buffer.AddOperation(op)
+
+ return true, nil
+}
+
+// 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) {
+ tx.mutex.Lock()
+ defer tx.mutex.Unlock()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return false, err
+ }
+
+ // Check if old policy exists and new policy doesn't exist.
+ bufferedModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+ if err != nil {
+ return false, err
+ }
+
+ hasOldPolicy, err := bufferedModel.HasPolicy("p", ptype, oldPolicy)
+ if err != nil {
+ return false, err
+ }
+ if !hasOldPolicy {
+ return false, nil
+ }
+
+ hasNewPolicy, errNew := bufferedModel.HasPolicy("p", ptype, newPolicy)
+ if errNew != nil {
+ return false, errNew
+ }
+ if hasNewPolicy {
+ return false, nil
+ }
+
+ // Add operation to buffer.
+ op := persist.PolicyOperation{
+ Type: persist.OperationUpdate,
+ Section: "p",
+ PolicyType: ptype,
+ Rules: [][]string{newPolicy},
+ OldRules: [][]string{oldPolicy},
+ }
+ tx.buffer.AddOperation(op)
+
+ return true, nil
+}
+
+// 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) {
+ tx.mutex.Lock()
+ defer tx.mutex.Unlock()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return false, err
+ }
+
+ rule := tx.buildRuleFromParams(params...)
+
+ // Check if grouping policy already exists in the buffered model.
+ bufferedModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+ if err != nil {
+ return false, err
+ }
+
+ hasPolicy, err := bufferedModel.HasPolicy("g", ptype, rule)
+ if hasPolicy || err != nil {
+ return false, err
+ }
+
+ // Add operation to buffer.
+ op := persist.PolicyOperation{
+ Type: persist.OperationAdd,
+ Section: "g",
+ PolicyType: ptype,
+ Rules: [][]string{rule},
+ }
+ tx.buffer.AddOperation(op)
+
+ return true, nil
+}
+
+// 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) {
+ tx.mutex.Lock()
+ defer tx.mutex.Unlock()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return false, err
+ }
+
+ rule := tx.buildRuleFromParams(params...)
+
+ // Check if grouping policy exists in the buffered model.
+ bufferedModel, err := tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+ if err != nil {
+ return false, err
+ }
+
+ hasPolicy, err := bufferedModel.HasPolicy("g", ptype, rule)
+ if !hasPolicy || err != nil {
+ return false, err
+ }
+
+ // Add operation to buffer.
+ op := persist.PolicyOperation{
+ Type: persist.OperationRemove,
+ Section: "g",
+ PolicyType: ptype,
+ Rules: [][]string{rule},
+ }
+ tx.buffer.AddOperation(op)
+
+ return true, nil
+}
+
+// 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()
+
+ if err := tx.checkTransactionStatus(); err != nil {
+ return nil, err
+ }
+
+ return tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+}
+
+// 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_buffer.go b/transaction_buffer.go
new file mode 100644
index 000000000..769ef925f
--- /dev/null
+++ b/transaction_buffer.go
@@ -0,0 +1,131 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..bdb6bd646
--- /dev/null
+++ b/transaction_commit.go
@@ -0,0 +1,264 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..6c12d8396
--- /dev/null
+++ b/transaction_conflict.go
@@ -0,0 +1,134 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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 000000000..5dcc1c0ae
--- /dev/null
+++ b/transaction_test.go
@@ -0,0 +1,337 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// 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.
+
+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()
+}
diff --git a/ui_model_editor.png b/ui_model_editor.png
deleted file mode 100644
index 2e36434c5..000000000
Binary files a/ui_model_editor.png and /dev/null differ
diff --git a/ui_policy_editor.png b/ui_policy_editor.png
deleted file mode 100644
index 0fdf90128..000000000
Binary files a/ui_policy_editor.png and /dev/null differ
diff --git a/util/builtin_operators.go b/util/builtin_operators.go
index 98b9d98ef..37d9cb5bf 100644
--- a/util/builtin_operators.go
+++ b/util/builtin_operators.go
@@ -15,15 +15,73 @@
package util
import (
+ "errors"
+ "fmt"
"net"
"regexp"
"strings"
+ "sync"
+ "time"
- "github.com/casbin/casbin/rbac"
+ "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{}
+)
+
+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/*"
+// For example, "/foo/bar" matches "/foo/*".
func KeyMatch(key1 string, key2 string) bool {
i := strings.Index(key2, "*")
if i == -1 {
@@ -38,60 +96,238 @@ func KeyMatch(key1 string, key2 string) bool {
// 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 (bool)(KeyMatch(name1, name2)), nil
+ 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"
+// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource".
func KeyMatch2(key1 string, key2 string) bool {
key2 = strings.Replace(key2, "/*", "/.*", -1)
- re := regexp.MustCompile(`(.*):[^/]+(.*)`)
- for {
- if !strings.Contains(key2, "/:") {
- break
- }
-
- key2 = "^" + re.ReplaceAllString(key2, "$1[^/]+$2") + "$"
- }
+ key2 = keyMatch2Re.ReplaceAllString(key2, "$1[^/]+$2")
- return RegexMatch(key1, key2)
+ return RegexMatch(key1, "^"+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 (bool)(KeyMatch2(name1, name2)), nil
+ return KeyMatch2(name1, name2), 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 {
+// 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 {
key2 = strings.Replace(key2, "/*", "/.*", -1)
+ keys := keyGet2Re1.FindAllString(key2, -1)
+ key2 = keyGet2Re1.ReplaceAllString(key2, "$1([^/]+)$2")
+ key2 = "^" + key2 + "$"
- re := regexp.MustCompile(`(.*)\{[^/]+\}(.*)`)
- for {
- if !strings.Contains(key2, "/{") {
- break
+ re := mustCompileOrGet(key2)
+ values := re.FindAllStringSubmatch(key1, -1)
+ if len(values) == 0 {
+ return ""
+ }
+ for i, key := range keys {
+ if pathVar == key[1:] {
+ return values[0][i+1]
}
+ }
+ return ""
+}
- key2 = re.ReplaceAllString(key2, "$1[^/]+$2")
+// 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)
}
- return RegexMatch(key1, key2)
+ 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 {
+ key2 = strings.Replace(key2, "/*", "/.*", -1)
+ key2 = keyMatch3Re.ReplaceAllString(key2, "$1[^/]+$2")
+
+ return RegexMatch(key1, "^"+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 {
+ key2 = strings.Replace(key2, "/*", "/.*", -1)
+
+ keys := keyGet3Re1.FindAllString(key2, -1)
+ key2 = keyGet3Re1.ReplaceAllString(key2, "$1([^/]+?)$2")
+ key2 = "^" + key2 + "$"
+ re := mustCompileOrGet(key2)
+ values := re.FindAllStringSubmatch(key1, -1)
+ if len(values) == 0 {
+ return ""
+ }
+ for i, key := range keys {
+ if pathVar == key[1:len(key)-1] {
+ return values[0][i+1]
+ }
+ }
+ return ""
+}
+
+// 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 {
+ key2 = strings.Replace(key2, "/*", "/.*", -1)
+
+ tokens := []string{}
+
+ re := keyMatch4Re
+ key2 = re.ReplaceAllStringFunc(key2, func(s string) string {
+ tokens = append(tokens, s[1:len(s)-1])
+ return "([^/]+)"
+ })
+
+ re = mustCompileOrGet("^" + key2 + "$")
+ matches := re.FindStringSubmatch(key1)
+ if matches == nil {
+ return false
+ }
+ matches = matches[1:]
+
+ if len(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 tokens {
+ if _, ok := values[token]; !ok {
+ values[token] = matches[key]
+ }
+ if values[token] != matches[key] {
+ return false
+ }
+ }
+
+ return true
+}
+
+// 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 (bool)(KeyMatch3(name1, name2)), nil
+ 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]
+ }
+
+ key2 = strings.Replace(key2, "/*", "/.*", -1)
+ key2 = keyMatch5Re.ReplaceAllString(key2, "$1[^/]+$2")
+
+ return RegexMatch(key1, "^"+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.
@@ -105,14 +341,18 @@ func RegexMatch(key1 string, key2 string) bool {
// 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 (bool)(RegexMatch(name1, name2)), nil
+ 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"
+// 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 {
@@ -134,27 +374,125 @@ func IPMatch(ip1 string, ip2 string) bool {
// 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 (bool)(IPMatch(ip1, ip2)), nil
+ return IPMatch(ip1, ip2), nil
}
-// GenerateGFunction is the factory method of the g(_, _) function.
-func GenerateGFunction(rm rbac.RoleManager) func(args ...interface{}) (interface{}, error) {
+// 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.
+func GenerateGFunction(rm rbac.RoleManager) govaluate.ExpressionFunction {
+ memorized := sync.Map{}
return func(args ...interface{}) (interface{}, error) {
- name1 := args[0].(string)
- name2 := args[1].(string)
+ // Like all our other govaluate functions, all args are strings.
+ // 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.
+ v, found := memorized.Load(key)
+ if 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)
if rm == nil {
- return name1 == name2, nil
+ v = name1 == name2
} else if len(args) == 2 {
- res, _ := rm.HasLink(name1, name2)
- return res, nil
+ v, _ = rm.HasLink(name1, name2)
} else {
domain := args[2].(string)
- res, _ := rm.HasLink(name1, name2, domain)
- return res, nil
+ v, _ = rm.HasLink(name1, name2, domain)
}
+
+ memorized.Store(key, v)
+ 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_test.go b/util/builtin_operators_test.go
index 3b5652ec0..ba0f463db 100644
--- a/util/builtin_operators_test.go
+++ b/util/builtin_operators_test.go
@@ -15,14 +15,13 @@
package util
import (
- "log"
"testing"
)
func testKeyMatch(t *testing.T, key1 string, key2 string, res bool) {
t.Helper()
myRes := KeyMatch(key1, key2)
- log.Printf("%s < %s: %t", key1, key2, myRes)
+ t.Logf("%s < %s: %t", key1, key2, myRes)
if myRes != res {
t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res)
@@ -41,10 +40,45 @@ func TestKeyMatch(t *testing.T) {
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)
- log.Printf("%s < %s: %t", key1, key2, myRes)
+ 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)
@@ -55,11 +89,11 @@ 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", true) // different with KeyMatch.
- testKeyMatch2(t, "/foo/bar", "/foo*", true)
+ 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", true) // different with KeyMatch.
- testKeyMatch2(t, "/foobar", "/foo*", true)
+ testKeyMatch2(t, "/foobar", "/foo", false)
+ testKeyMatch2(t, "/foobar", "/foo*", false) // different with KeyMatch.
testKeyMatch2(t, "/foobar", "/foo/*", false)
testKeyMatch2(t, "/", "/:resource", false)
@@ -78,12 +112,57 @@ func TestKeyMatch2(t *testing.T) {
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)
- log.Printf("%s < %s: %t", key1, key2, myRes)
+ t.Logf("%s < %s: %t", key1, key2, myRes)
if myRes != res {
t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res)
@@ -91,14 +170,15 @@ func testKeyMatch3(t *testing.T, key1 string, key2 string, res bool) {
}
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", true) // different with KeyMatch.
- testKeyMatch3(t, "/foo/bar", "/foo*", true)
+ testKeyMatch3(t, "/foo/bar", "/foo", false)
+ testKeyMatch3(t, "/foo/bar", "/foo*", false)
testKeyMatch3(t, "/foo/bar", "/foo/*", true)
- testKeyMatch3(t, "/foobar", "/foo", true) // different with KeyMatch.
- testKeyMatch3(t, "/foobar", "/foo*", true)
+ testKeyMatch3(t, "/foobar", "/foo", false)
+ testKeyMatch3(t, "/foobar", "/foo*", false)
testKeyMatch3(t, "/foobar", "/foo/*", false)
testKeyMatch3(t, "/", "/{resource}", false)
@@ -112,12 +192,90 @@ func TestKeyMatch3(t *testing.T) {
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)
- log.Printf("%s < %s: %t", key1, key2, myRes)
+ t.Logf("%s < %s: %t", key1, key2, myRes)
if myRes != res {
t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res)
@@ -139,7 +297,7 @@ func TestRegexMatch(t *testing.T) {
func testIPMatch(t *testing.T, ip1 string, ip2 string, res bool) {
t.Helper()
myRes := IPMatch(ip1, ip2)
- log.Printf("%s < %s: %t", ip1, ip2, myRes)
+ t.Logf("%s < %s: %t", ip1, ip2, myRes)
if myRes != res {
t.Errorf("%s < %s: %t, supposed to be %t", ip1, ip2, !res, res)
@@ -155,3 +313,352 @@ func TestIPMatch(t *testing.T) {
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 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 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 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 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 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 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
index 6c4fc902a..b72823a4c 100644
--- a/util/util.go
+++ b/util/util.go
@@ -15,14 +15,36 @@
package util
import (
+ "encoding/json"
+ "regexp"
"sort"
"strings"
+ "sync"
)
+var evalReg = regexp.MustCompile(`\beval\((?P