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/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 96603a39e..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,8 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: casbin -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: # Replace with a single custom sponsorship URL 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/.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 0e1315123..000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go - -sudo: false - -go: - - "1.11" - - "1.12" - -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 5a96ea120..89dc9af8e 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ Casbin ==== [![Go Report Card](https://goreportcard.com/badge/github.com/casbin/casbin)](https://goreportcard.com/report/github.com/casbin/casbin) -[![Build Status](https://travis-ci.org/casbin/casbin.svg?branch=master)](https://travis-ci.org/casbin/casbin) +[![Build](https://github.com/casbin/casbin/actions/workflows/default.yml/badge.svg)](https://github.com/casbin/casbin/actions/workflows/default.yml) [![Coverage Status](https://coveralls.io/repos/github/casbin/casbin/badge.svg?branch=master)](https://coveralls.io/github/casbin/casbin?branch=master) -[![Godoc](https://godoc.org/github.com/casbin/casbin?status.svg)](https://godoc.org/github.com/casbin/casbin) +[![Godoc](https://godoc.org/github.com/casbin/casbin?status.svg)](https://pkg.go.dev/github.com/casbin/casbin/v2) [![Release](https://img.shields.io/github/release/casbin/casbin.svg)](https://github.com/casbin/casbin/releases/latest) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/casbin/lobby) +[![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN) [![Sourcegraph](https://sourcegraph.com/github.com/casbin/casbin/-/badge.svg)](https://sourcegraph.com/github.com/casbin/casbin?badge) -**News**: still worry about how to write the correct Casbin policy? ``Casbin online editor`` is coming to help! Try it at: http://casbin.org/editor/ +**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 Logo](casbin-logo.png) @@ -17,15 +17,15 @@ Casbin is a powerful and efficient open-source access control library for Golang ## All the languages supported by Casbin: -[![golang](https://casbin.org/img/langs/golang.png)](https://github.com/casbin/casbin) | [![java](https://casbin.org/img/langs/java.png)](https://github.com/casbin/jcasbin) | [![nodejs](https://casbin.org/img/langs/nodejs.png)](https://github.com/casbin/node-casbin) | [![php](https://casbin.org/img/langs/php.png)](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 +| [![golang](https://casbin.org/img/langs/golang.png)](https://github.com/casbin/casbin) | [![java](https://casbin.org/img/langs/java.png)](https://github.com/casbin/jcasbin) | [![nodejs](https://casbin.org/img/langs/nodejs.png)](https://github.com/casbin/node-casbin) | [![php](https://casbin.org/img/langs/php.png)](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 | -[![python](https://casbin.org/img/langs/python.png)](https://github.com/casbin/pycasbin) | [![dotnet](https://casbin.org/img/langs/dotnet.png)](https://github.com/casbin-net/Casbin.NET) | [![delphi](https://casbin.org/img/langs/delphi.png)](https://github.com/casbin4d/Casbin4D) | [![rust](https://casbin.org/img/langs/rust.png)](https://github.com/Devolutions/casbin-rs) -----|----|----|---- -[PyCasbin](https://github.com/casbin/pycasbin) | [Casbin.NET](https://github.com/casbin-net/Casbin.NET) | [Casbin4D](https://github.com/casbin4d/Casbin4D) | [Casbin-RS](https://github.com/Devolutions/casbin-rs) -production-ready | production-ready | experimental | WIP +| [![python](https://casbin.org/img/langs/python.png)](https://github.com/casbin/pycasbin) | [![dotnet](https://casbin.org/img/langs/dotnet.png)](https://github.com/casbin-net/Casbin.NET) | [![c++](https://casbin.org/img/langs/cpp.png)](https://github.com/casbin/casbin-cpp) | [![rust](https://casbin.org/img/langs/rust.png)](https://github.com/casbin/casbin-rs) | +|------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------| +| [PyCasbin](https://github.com/casbin/pycasbin) | [Casbin.NET](https://github.com/casbin-net/Casbin.NET) | [Casbin-CPP](https://github.com/casbin/casbin-cpp) | [Casbin-RS](https://github.com/casbin/casbin-rs) | +| production-ready | production-ready | production-ready | production-ready | ## Table of contents @@ -97,12 +97,12 @@ It means: - alice can read data1 - bob can write data2 -We also support multi-line mode by appending '\\' in the end: +We also support multi-line mode by appending '\\' in the end: ```ini # Matchers [matchers] -m = r.sub == p.sub && r.obj == p.obj \ +m = r.sub == p.sub && r.obj == p.obj \ && r.act == p.act ``` @@ -116,7 +116,7 @@ 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/Knetic/govaluate) +For more operators, you may take a look at [govaluate](https://github.com/casbin/govaluate) ## Features @@ -125,31 +125,31 @@ 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 -https://casbin.org/docs/en/overview +https://casbin.org/docs/overview ## Online editor -You can also use the online editor (http://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. +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 -https://casbin.org/docs/en/tutorials +https://casbin.org/docs/tutorials ## Get started @@ -159,7 +159,7 @@ https://casbin.org/docs/en/tutorials 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: @@ -168,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 res := e.Enforce(sub, obj, act); res { + if res, _ := e.Enforce(sub, obj, act); res { // permit alice to read data1 } else { // deny the request, show an error @@ -183,16 +183,14 @@ Note: you can also initialize an enforcer with policy in DB instead of file, see 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: ![model editor](https://hsluoyz.github.io/casbin/ui_model_editor.png) @@ -200,69 +198,56 @@ We also provide a web-based UI for model management and policy management: ## Policy persistence -https://casbin.org/docs/en/adapters +https://casbin.org/docs/adapters ## Policy consistence between multiple nodes -https://casbin.org/docs/en/watchers +https://casbin.org/docs/watchers ## Role manager -https://casbin.org/docs/en/role-managers +https://casbin.org/docs/role-managers ## Benchmarks -https://casbin.org/docs/en/benchmark +https://casbin.org/docs/benchmark ## Examples -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) +| 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) | ## Middlewares -Authz middlewares for web frameworks: https://casbin.org/docs/en/middlewares +Authz middlewares for web frameworks: https://casbin.org/docs/middlewares ## Our adopters -https://casbin.org/docs/en/adopters - -## Contributors +https://casbin.org/docs/adopters -This project exists thanks to all the people who contribute. - +## How to Contribute -## Backers +Please read the [contributing guide](CONTRIBUTING.md). -Thank you to all our backers! šŸ™ [[Become a backer](https://opencollective.com/casbin#backer)] - - +## Contributors -## Sponsors +This project exists thanks to all the people who contribute. + -Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/casbin#sponsor)] +## Star History - - - - - - - - - - +[![Star History Chart](https://api.star-history.com/svg?repos=casbin/casbin&type=Date)](https://star-history.com/#casbin/casbin&Date) ## License @@ -272,5 +257,4 @@ This project is licensed under the [Apache 2.0 license](LICENSE). 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) +- 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 f188dfc85..57d40d849 100644 --- a/config/config.go +++ b/config/config.go @@ -23,21 +23,20 @@ import ( "os" "strconv" "strings" - "sync" ) var ( - // DEFAULT_SECTION specifies the name of a section if no name provided + // 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 defines what character(s) indicate a comment `#`. DEFAULT_COMMENT = []byte{'#'} - // DEFAULT_COMMENT_SEM defines what alternate character(s) indicate a comment `;` + // 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 defines what character indicates a multi-line content. DEFAULT_MULTI_LINE_SEPARATOR = []byte{'\\'} ) -// ConfigInterface defines the behavior of a Config implemenation +// ConfigInterface defines the behavior of a Config implementation. type ConfigInterface interface { String(key string) string Strings(key string) []string @@ -48,10 +47,8 @@ type ConfigInterface interface { Set(key string, value string) error } -// Config represents an implementation of the ConfigInterface +// 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 } @@ -91,12 +88,10 @@ 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) @@ -121,7 +116,7 @@ func (c *Config) parseBuffer(buf *bufio.Reader) error { 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 { + if err = c.write(section, lineNum, &buffer); err != nil { return err } } @@ -149,12 +144,20 @@ func (c *Config) parseBuffer(buf *bufio.Reader) error { 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 } - if _, err := buffer.Write(p); err != nil { + 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 } } @@ -182,33 +185,33 @@ func (c *Config) write(section string, lineNum int, b *bytes.Buffer) error { return nil } -// Bool lookups up the value using the provided key and converts the value to a bool +// 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 +// 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 +// 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 +// 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 +// 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 +// by splitting the string by comma. func (c *Config) Strings(key string) []string { v := c.get(key) if v == "" { @@ -217,10 +220,8 @@ func (c *Config) Strings(key string) []string { return strings.Split(v, ",") } -// Set sets the value for the specific key in the Config +// 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") } @@ -242,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 f0f3158af..8431ae10c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -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,23 @@ func TestGet(t *testing.T) { t.Fatalf("err: %v", err) } - config.Set("other::key1", "new test key") + _ = config.Set("other::key1", "new test key") if v := config.String("other::key1"); v != "new test key" { t.Errorf("Get failure: expected different value for other::key1 (expected: [%#v] got: [%#v])", "new test key", v) } - config.Set("other::key1", "test key") + _ = config.Set("other::key1", "test key") - if v := config.String("multi1::name"); v != "r.sub==p.sub&&r.obj==p.obj" { + if v := config.String("multi1::name"); v != "r.sub==p.sub && r.obj==p.obj" { t.Errorf("Get failure: expected different value for multi1::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v) } - if v := config.String("multi2::name"); v != "r.sub==p.sub&&r.obj==p.obj" { + if v := config.String("multi2::name"); v != "r.sub==p.sub && r.obj==p.obj" { t.Errorf("Get failure: expected different value for multi2::name (expected: [%#v] got: [%#v])", "r.sub==p.sub&&r.obj==p.obj", v) } - if v := config.String("multi3::name"); v != "r.sub==p.sub&&r.obj==p.obj" { + 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) } @@ -79,7 +84,7 @@ func TestGet(t *testing.T) { t.Errorf("Get failure: expected different value for multi4::name (expected: [%#v] got: [%#v])", "", v) } - if v := config.String("multi5::name"); v != "r.sub==p.sub&&r.obj==p.obj" { + 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 733e544b9..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 @@ -26,15 +26,15 @@ math.f64 = 64.1 # multi-line test [multi1] name = r.sub==p.sub \ - &&r.obj==p.obj\ + && r.obj==p.obj\ \ [multi2] name = r.sub==p.sub \ - &&r.obj==p.obj + && r.obj==p.obj [multi3] name = r.sub==p.sub \ - &&r.obj==p.obj + && r.obj==p.obj [multi4] name = \ @@ -43,5 +43,5 @@ name = \ [multi5] name = r.sub==p.sub \ - &&r.obj==p.obj\ - \ \ No newline at end of file + && 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 bf8ae80d5..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 "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 2bbc27962..a6bf1740a 100644 --- a/enforcer.go +++ b/enforcer.go @@ -17,16 +17,21 @@ package casbin import ( "errors" "fmt" - - "github.com/Knetic/govaluate" - "github.com/casbin/casbin/v2/effect" - "github.com/casbin/casbin/v2/log" - "github.com/casbin/casbin/v2/model" - "github.com/casbin/casbin/v2/persist" - fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" - "github.com/casbin/casbin/v2/rbac" - defaultrolemanager "github.com/casbin/casbin/v2/rbac/default-role-manager" - "github.com/casbin/casbin/v2/util" + "runtime/debug" + "strings" + "sync" + + "github.com/casbin/casbin/v3/detector" + "github.com/casbin/casbin/v3/effector" + "github.com/casbin/casbin/v3/log" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" + "github.com/casbin/casbin/v3/rbac" + defaultrolemanager "github.com/casbin/casbin/v3/rbac/default-role-manager" + "github.com/casbin/casbin/v3/util" + + "github.com/casbin/govaluate" ) // Enforcer is the main interface for authorization enforcement and policy management. @@ -34,37 +39,57 @@ 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 + + aiConfig AIConfig +} - adapter persist.Adapter - watcher persist.Watcher - rm rbac.RoleManager +// 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.csv") +// +// 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) +// +// 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{} 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 paramLen - parsedParamLen { + case 2: switch p0 := params[0].(type) { case string: switch p1 := params[1].(type) { @@ -90,7 +115,7 @@ func NewEnforcer(params ...interface{}) (*Enforcer, error) { } } } - } else if len(params)-parsedParamLen == 1 { + case 1: switch p0 := params[0].(type) { case string: err := e.InitWithFile(p0, "") @@ -103,9 +128,9 @@ func NewEnforcer(params ...interface{}) (*Enforcer, error) { return nil, err } } - } else if len(params)-parsedParamLen == 0 { + case 0: return e, nil - } else { + default: return nil, errors.New("invalid parameters for enforcer") } @@ -157,13 +182,23 @@ func (e *Enforcer) InitWithModelAndAdapter(m model.Model, adapter persist.Adapte } func (e *Enforcer) initialize() { - e.rm = defaultrolemanager.NewRoleManager(10) - e.eft = effect.NewDefaultEffector() + e.rmMap = map[string]rbac.RoleManager{} + e.condRmMap = map[string]rbac.ConditionalRoleManager{} + e.eft = effector.NewDefaultEffector() e.watcher = nil + e.matcherMap = sync.Map{} e.enabled = true e.autoSave = true e.autoBuildRoleLinks = true + e.autoNotifyWatcher = true + e.autoNotifyDispatcher = true + e.initRmMap() + + // Initialize detectors with default detector if not already set + if e.detectors == nil { + e.detectors = []detector.Detector{detector.NewDefaultDetector()} + } } // LoadModel reloads the model from the model CONF file. @@ -178,6 +213,8 @@ func (e *Enforcer) LoadModel() error { e.model.PrintModel() e.fm = model.LoadFunctionMap() + e.initialize() + return nil } @@ -190,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. @@ -205,44 +244,231 @@ func (e *Enforcer) SetAdapter(adapter persist.Adapter) { // SetWatcher sets the current watcher. func (e *Enforcer) SetWatcher(watcher persist.Watcher) error { e.watcher = watcher - return 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() - if err := e.adapter.LoadPolicy(e.model); err != nil && err.Error() != "invalid file path, file path cannot be empty" { + 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 { - err := 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 +} + +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 } -// LoadFilteredPolicy reloads a filtered policy from file/database. -func (e *Enforcer) LoadFilteredPolicy(filter interface{}) error { - e.model.ClearPolicy() +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 @@ -257,6 +483,15 @@ func (e *Enforcer) LoadFilteredPolicy(filter interface{}) error { 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() @@ -267,6 +502,18 @@ func (e *Enforcer) LoadFilteredPolicy(filter interface{}) error { 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) @@ -278,26 +525,101 @@ 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 } if err := e.adapter.SavePolicy(e.model); err != nil { + e.onLogAfterEventWithError(logEntry, err) return err } + + e.onLogAfterEventInSavePolicy(logEntry) + if e.watcher != nil { - return e.watcher.Update() + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForSavePolicy(e.model) + } else { + err = e.watcher.Update() + } + return err } return nil } +// getDomainTokens extracts domain token names from request and policy definitions. +// Returns empty strings if tokens cannot be found. +func (e *Enforcer) getDomainTokens() (rDomainToken, pDomainToken string) { + if rAssertion, ok := e.model["r"]["r"]; ok && len(rAssertion.Tokens) > 1 { + rDomainToken = rAssertion.Tokens[1] + } + if pAssertion, ok := e.model["p"]["p"]; ok && len(pAssertion.Tokens) > 1 { + pDomainToken = pAssertion.Tokens[1] + } + return rDomainToken, pDomainToken +} + +// registerDomainMatchingFunc registers domain matching function if the matcher uses keyMatch for domains. +func (e *Enforcer) registerDomainMatchingFunc(ptype string) { + // Dynamically detect the domain token name from the model definition. + // In RBAC with domains, the domain is typically the second parameter (index 1) + // in both request and policy definitions (e.g., r = sub, dom, obj, act). + // We extract the actual token names to support arbitrary domain parameter names. + rDomainToken, pDomainToken := e.getDomainTokens() + if rDomainToken == "" || pDomainToken == "" { + return + } + + matchFun := fmt.Sprintf("keyMatch(%s, %s)", rDomainToken, pDomainToken) + if strings.Contains(e.model["m"]["m"].Value, matchFun) { + e.AddNamedDomainMatchingFunc(ptype, "g", util.KeyMatch) + } +} + +func (e *Enforcer) initRmMap() { + for ptype, assertion := range e.model["g"] { + if rm, ok := e.rmMap[ptype]; ok { + _ = rm.Clear() + continue + } + if len(assertion.Tokens) <= 2 && len(assertion.ParamsTokens) == 0 { + assertion.RM = defaultrolemanager.NewRoleManagerImpl(10) + e.rmMap[ptype] = assertion.RM + } + if len(assertion.Tokens) <= 2 && len(assertion.ParamsTokens) != 0 { + assertion.CondRM = defaultrolemanager.NewConditionalRoleManager(10) + e.condRmMap[ptype] = assertion.CondRM + } + if len(assertion.Tokens) > 2 { + if len(assertion.ParamsTokens) == 0 { + assertion.RM = defaultrolemanager.NewRoleManager(10) + e.rmMap[ptype] = assertion.RM + } else { + assertion.CondRM = defaultrolemanager.NewConditionalDomainManager(10) + e.condRmMap[ptype] = assertion.CondRM + } + e.registerDomainMatchingFunc(ptype) + } + } +} + // EnableEnforce changes the enforcing state of Casbin, when Casbin is disabled, all access will be allowed by the Enforce() function. func (e *Enforcer) EnableEnforce(enable bool) { e.enabled = enable } -// EnableLog changes whether Casbin will log messages to the Logger. -func (e *Enforcer) EnableLog(enable bool) { - log.GetLogger().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. @@ -310,48 +632,142 @@ 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() error { - err := e.rm.Clear() - if err != nil { - return err + 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.rm) + 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, error) { +// BuildIncrementalRoleLinks provides incremental build the role inheritance relations. +func (e *Enforcer) BuildIncrementalRoleLinks(op model.PolicyOp, ptype string, rules [][]string) error { + e.invalidateMatcherMap() + return e.model.BuildIncrementalRoleLinks(e.rmMap, op, "g", ptype, rules) +} + +// BuildIncrementalConditionalRoleLinks provides incremental build the role inheritance relations with conditions. +func (e *Enforcer) BuildIncrementalConditionalRoleLinks(op model.PolicyOp, ptype string, rules [][]string) error { + e.invalidateMatcherMap() + return e.model.BuildIncrementalConditionalRoleLinks(e.condRmMap, op, "g", ptype, rules) +} + +// NewEnforceContext Create a default structure based on the suffix. +func NewEnforceContext(suffix string) EnforceContext { + return EnforceContext{ + RType: "r" + suffix, + PType: "p" + suffix, + EType: "e" + suffix, + MType: "m" + suffix, + } +} + +func (e *Enforcer) invalidateMatcherMap() { + e.matcherMap = sync.Map{} +} + +// enforce use a custom matcher to decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (matcher, sub, obj, act), use model matcher by default when matcher is "". +func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interface{}) (ok bool, err error) { //nolint:funlen,cyclop,gocyclo // TODO: reduce function complexity + logEntry := e.onLogBeforeEventInEnforce(rvals) + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v\n%s", r, debug.Stack()) + if e.logger != nil && logEntry != nil { + logEntry.Error = err + } + } + e.onLogAfterEventInEnforce(logEntry, ok) + }() + if !e.enabled { return true, nil } - functions := make(map[string]govaluate.ExpressionFunction) - for key, function := range e.fm { - functions[key] = function - } + 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, err := govaluate.NewEvaluableExpressionWithFunctions(expString, functions) - if err != nil { - return false, err + var ( + rType = "r" + pType = "p" + eType = "e" + mType = "m" + ) + if len(rvals) != 0 { + switch rvals[0].(type) { + case EnforceContext: + enforceContext := rvals[0].(EnforceContext) + rType = enforceContext.RType + pType = enforceContext.PType + eType = enforceContext.EType + mType = enforceContext.MType + rvals = rvals[1:] + default: + break + } + } + + var expString string + if matcher == "" { + expString = e.model["m"][mType].Value + } else { + // For custom matchers provided at runtime, escape backslashes in string literals + expString = util.EscapeStringLiterals(util.RemoveComments(util.EscapeAssertion(matcher))) } - rTokens := make(map[string]int, len(e.model["r"]["r"].Tokens)) - for i, token := range e.model["r"]["r"].Tokens { + 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"]["p"].Tokens)) - for i, token := range e.model["p"]["p"].Tokens { + pTokens := make(map[string]int, len(e.model["p"][pType].Tokens)) + for i, token := range e.model["p"][pType].Tokens { pTokens[token] = i } + if e.acceptJsonRequest { + // try to parse all request values from json to map[string]interface{} + for i, rval := range rvals { + switch rval := rval.(type) { + case string: + // Only attempt JSON parsing for strings that look like JSON objects or arrays + if len(rval) > 0 && (rval[0] == '{' || rval[0] == '[') { + var mapValue map[string]interface{} + mapValue, err = util.JsonToMap(rval) + if err != nil { + // Return a clear error when JSON-like string fails to parse + return false, fmt.Errorf("failed to parse JSON parameter at index %d: %w", i, err) + } + rvals[i] = mapValue + } + } + } + } + parameters := enforceParameters{ rTokens: rTokens, rVals: rvals, @@ -359,28 +775,42 @@ func (e *Enforcer) Enforce(rvals ...interface{}) (bool, error) { pTokens: pTokens, } - var policyEffects []effect.Effect + 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 - if policyLen := len(e.model["p"]["p"].Policy); policyLen != 0 { - policyEffects = make([]effect.Effect, policyLen) + + 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) - if len(e.model["r"]["r"].Tokens) != len(rvals) { - return false, errors.New( - fmt.Sprintf( - "invalid request size: expected %d, got %d, rvals: %v", - len(e.model["r"]["r"].Tokens), - len(rvals), - rvals)) - } - for i, pvals := range e.model["p"]["p"].Policy { + + for policyIndex, pvals := range e.model["p"][pType].Policy { // log.LogPrint("Policy Rule: ", pvals) - if len(e.model["p"]["p"].Tokens) != len(pvals) { - return false, errors.New( - fmt.Sprintf( - "invalid policy size: expected %d, got %d, pvals: %v", - len(e.model["p"]["p"].Tokens), - len(pvals), - pvals)) + if len(e.model["p"][pType].Tokens) != len(pvals) { + return false, fmt.Errorf( + "invalid policy size: expected %d, got %d, pvals: %v", + len(e.model["p"][pType].Tokens), + len(pvals), + pvals) } parameters.pVals = pvals @@ -392,86 +822,219 @@ func (e *Enforcer) Enforce(rvals ...interface{}) (bool, error) { return false, err } + // set to no-match at first + matcherResults[policyIndex] = 0 switch result := result.(type) { case bool: - if !result { - policyEffects[i] = effect.Indeterminate - continue + if result { + matcherResults[policyIndex] = 1 } case float64: - if result == 0 { - policyEffects[i] = effect.Indeterminate - continue - } else { - matcherResults[i] = result + if result != 0 { + matcherResults[policyIndex] = 1 } default: return false, errors.New("matcher result should be bool, int or float") } - if j, ok := parameters.pTokens["p_eft"]; ok { + if j, ok := parameters.pTokens[pType+"_eft"]; ok { eft := parameters.pVals[j] if eft == "allow" { - policyEffects[i] = effect.Allow + policyEffects[policyIndex] = effector.Allow } else if eft == "deny" { - policyEffects[i] = effect.Deny + policyEffects[policyIndex] = effector.Deny } else { - policyEffects[i] = effect.Indeterminate + policyEffects[policyIndex] = effector.Indeterminate } } else { - policyEffects[i] = effect.Allow + policyEffects[policyIndex] = effector.Allow } - if e.model["e"]["e"].Value == "priority(p_eft) || deny" { + // 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) - // log.LogPrint("Result: ", result) if err != nil { return false, err } if result.(bool) { - policyEffects[0] = effect.Allow + policyEffects[0] = effector.Allow } else { - policyEffects[0] = effect.Indeterminate + 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 } } - // log.LogPrint("Rule Results: ", policyEffects) + if explains != nil { + if explainIndex != -1 && len(e.model["p"][pType].Policy) > explainIndex { + *explains = e.model["p"][pType].Policy[explainIndex] + } + } - result, err := e.eft.MergeEffects(e.model["e"]["e"].Value, policyEffects, matcherResults) - if err != nil { - return false, err + // effect -> result + result := false + if effect == effector.Allow { + result = true } - // Log request. - if log.GetLogger().IsEnabled() { - reqStr := "Request: " - for i, rval := range rvals { - if i != len(rvals)-1 { - reqStr += fmt.Sprintf("%v, ", rval) - } else { - reqStr += fmt.Sprintf("%v", rval) - } + return result, nil +} + +func (e *Enforcer) getAndStoreMatcherExpression(hasEval bool, expString string, functions map[string]govaluate.ExpressionFunction) (*govaluate.EvaluableExpression, error) { + var expression *govaluate.EvaluableExpression + var err error + var cachedExpression, isPresent = e.matcherMap.Load(expString) + + if !hasEval && isPresent { + expression = cachedExpression.(*govaluate.EvaluableExpression) + } else { + expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions) + if err != nil { + return nil, err } - reqStr += fmt.Sprintf(" ---> %t", result) - log.LogPrint(reqStr) + e.matcherMap.Store(expString, expression) } + return expression, nil +} - return result, nil +// Enforce decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (sub, obj, act). +func (e *Enforcer) Enforce(rvals ...interface{}) (bool, error) { + return e.enforce("", nil, rvals...) +} + +// EnforceWithMatcher use a custom matcher to decides whether a "subject" can access a "object" with the operation "action", input parameters are usually: (matcher, sub, obj, act), use model matcher by default when matcher is "". +func (e *Enforcer) EnforceWithMatcher(matcher string, rvals ...interface{}) (bool, error) { + return e.enforce(matcher, nil, rvals...) +} + +// EnforceEx explain enforcement by informing matched rules. +func (e *Enforcer) EnforceEx(rvals ...interface{}) (bool, []string, error) { + explain := []string{} + result, err := e.enforce("", &explain, rvals...) + return result, explain, err +} + +// EnforceExWithMatcher use a custom matcher and explain enforcement by informing matched rules. +func (e *Enforcer) EnforceExWithMatcher(matcher string, rvals ...interface{}) (bool, []string, error) { + explain := []string{} + result, err := e.enforce(matcher, &explain, rvals...) + return result, explain, err +} + +// BatchEnforce enforce in batches. +func (e *Enforcer) BatchEnforce(requests [][]interface{}) ([]bool, error) { + var results []bool + for _, request := range requests { + result, err := e.enforce("", nil, request...) + if err != nil { + return results, err + } + results = append(results, result) + } + return results, nil +} + +// BatchEnforceWithMatcher enforce with matcher in batches. +func (e *Enforcer) BatchEnforceWithMatcher(matcher string, requests [][]interface{}) ([]bool, error) { + var results []bool + for _, request := range requests { + result, err := e.enforce(matcher, nil, request...) + if err != nil { + return results, err + } + results = append(results, result) + } + return results, nil +} + +// AddNamedMatchingFunc add MatchingFunc by ptype RoleManager. +func (e *Enforcer) AddNamedMatchingFunc(ptype, name string, fn rbac.MatchingFunc) bool { + if rm, ok := e.rmMap[ptype]; ok { + rm.AddMatchingFunc(name, fn) + return true + } + return false +} + +// AddNamedDomainMatchingFunc add MatchingFunc by ptype to RoleManager. +func (e *Enforcer) AddNamedDomainMatchingFunc(ptype, name string, fn rbac.MatchingFunc) bool { + if rm, ok := e.rmMap[ptype]; ok { + rm.AddDomainMatchingFunc(name, fn) + return true + } + if condRm, ok := e.condRmMap[ptype]; ok { + condRm.AddDomainMatchingFunc(name, fn) + return true + } + return false } -// assumes bounds have already been checked +// 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{} @@ -480,7 +1043,7 @@ type enforceParameters struct { pVals []string } -// implements govaluate.Parameters +// implements govaluate.Parameters. func (p enforceParameters) Get(name string) (interface{}, error) { if name == "" { return nil, nil @@ -503,3 +1066,22 @@ func (p enforceParameters) Get(name string) (interface{}, error) { 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 index 77ec40b42..2c230db13 100644 --- a/enforcer_cached.go +++ b/enforcer_cached.go @@ -15,17 +15,27 @@ package casbin import ( + "strings" "sync" + "sync/atomic" + "time" + + "github.com/casbin/casbin/v3/persist/cache" ) -// CachedEnforcer wraps Enforcer and provides decision cache +// CachedEnforcer wraps Enforcer and provides decision cache. type CachedEnforcer struct { *Enforcer - m map[string]bool - enableCache bool + 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{} @@ -35,60 +45,141 @@ func NewCachedEnforcer(params ...interface{}) (*CachedEnforcer, error) { return nil, err } - e.enableCache = true - e.m = make(map[string]bool) + 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) { - e.enableCache = enableCache + 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 , ingore the cache +// if rvals is not string , ignore the cache. func (e *CachedEnforcer) Enforce(rvals ...interface{}) (bool, error) { - if !e.enableCache { + if atomic.LoadInt32(&e.enableCache) == 0 { return e.Enforcer.Enforce(rvals...) } - key := "" - for _, rval := range rvals { - if val, ok := rval.(string); ok { - key += val + "$$" - } else { - return e.Enforcer.Enforce(rvals...) - } + key, ok := e.getKey(rvals...) + if !ok { + return e.Enforcer.Enforce(rvals...) } - if res, ok := e.getCachedResult(key); ok { + if res, err := e.getCachedResult(key); err == nil { return res, nil - } else { - res, err := e.Enforcer.Enforce(rvals...) - if err != nil { - return false, err + } 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() +} - e.setCachedResult(key, res) - return res, nil +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) getCachedResult(key string) (res bool, ok bool) { - e.locker.RLock() - defer e.locker.RUnlock() - res, ok = e.m[key] - return res, ok +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) setCachedResult(key string, res bool) { +func (e *CachedEnforcer) getCachedResult(key string) (res bool, err error) { e.locker.Lock() defer e.locker.Unlock() - e.m[key] = res + 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() { - e.m = make(map[string]bool) +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 index 800840413..2e2f780b5 100644 --- a/enforcer_cached_b_test.go +++ b/enforcer_cached_b_test.go @@ -30,7 +30,7 @@ func BenchmarkCachedBasicModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = e.Enforce("alice", "data1", "read") } } @@ -39,67 +39,89 @@ func BenchmarkCachedRBACModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data2", "read") + _, _ = e.Enforce("alice", "data2", "read") } } func BenchmarkCachedRBACModelSmall(b *testing.B) { e, _ := NewCachedEnforcer("examples/rbac_model.conf") - // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call. - e.EnableAutoBuildRoleLinks(false) // 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 BenchmarkCachedRBACModelMedium(b *testing.B) { e, _ := NewCachedEnforcer("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++ { - 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", "data150", "read") } } func BenchmarkCachedRBACModelLarge(b *testing.B) { e, _ := NewCachedEnforcer("examples/rbac_model.conf") - // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call. - e.EnableAutoBuildRoleLinks(false) + // 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", "data1500", "read") } } @@ -108,7 +130,7 @@ func BenchmarkCachedRBACModelWithResourceRoles(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = e.Enforce("alice", "data1", "read") } } @@ -117,7 +139,7 @@ func BenchmarkCachedRBACModelWithDomains(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "domain1", "data1", "read") + _, _ = e.Enforce("alice", "domain1", "data1", "read") } } @@ -127,7 +149,7 @@ func BenchmarkCachedABACModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", data1, "read") + _, _ = e.Enforce("alice", data1, "read") } } @@ -136,7 +158,7 @@ func BenchmarkCachedKeyMatchModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "/alice_data/resource1", "GET") + _, _ = e.Enforce("alice", "/alice_data/resource1", "GET") } } @@ -145,7 +167,7 @@ func BenchmarkCachedRBACModelWithDeny(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = e.Enforce("alice", "data1", "read") } } @@ -154,28 +176,46 @@ func BenchmarkCachedPriorityModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = 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") - // Do not rebuild the role inheritance relations for every AddGroupingPolicy() call. - e.EnableAutoBuildRoleLinks(false) - // 1000 roles, 100 resources. - for i := 0; i < 1000; i++ { - e.AddPolicy(fmt.Sprintf("group%d", i), fmt.Sprintf("data%d", i/10), "read") - } - // 10000 users. + + // 10000 roles, 1000 resources. + pPolicies := make([][]string, 0) for i := 0; i < 10000; i++ { - e.AddGroupingPolicy(fmt.Sprintf("user%d", i), fmt.Sprintf("group%d", i/10)) + 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) } - e.BuildRoleLinks() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - e.Enforce("user5001", "data150", "read") + _, _ = 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 index 8a9a54667..34da2fa0f 100644 --- a/enforcer_cached_test.go +++ b/enforcer_cached_test.go @@ -32,21 +32,42 @@ func TestCache(t *testing.T) { testEnforceCache(t, e, "alice", "data2", "read", false) testEnforceCache(t, e, "alice", "data2", "write", false) - // The cache is enabled, so even if we remove a policy rule, the decision - // for ("alice", "data1", "read") will still be true, as it uses the cached result. - e.RemovePolicy("alice", "data1", "read") + // 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", true) + 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) - // Now we invalidate the cache, then all first-coming Enforce() has to be evaluated in real-time. - // The decision for ("alice", "data1", "read") will be false now. - e.InvalidateCache() + 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, "alice", "data1", "write", 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_synced.go b/enforcer_synced.go index 9d8fe382f..89bbe5dae 100644 --- a/enforcer_synced.go +++ b/enforcer_synced.go @@ -15,18 +15,22 @@ package casbin import ( - "log" "sync" + "sync/atomic" "time" - "github.com/casbin/casbin/v2/persist" + "github.com/casbin/govaluate" + + "github.com/casbin/casbin/v3/persist" + "github.com/casbin/casbin/v3/rbac" ) -// SyncedEnforcer wraps Enforcer and provides synchronized access +// SyncedEnforcer wraps Enforcer and provides synchronized access. type SyncedEnforcer struct { *Enforcer - m sync.RWMutex - autoLoad bool + m sync.RWMutex + stopAutoLoad chan struct{} + autoLoadRunning int32 } // NewSyncedEnforcer creates a synchronized enforcer via file or DB. @@ -38,41 +42,97 @@ func NewSyncedEnforcer(params ...interface{}) (*SyncedEnforcer, error) { return nil, err } - e.autoLoad = false + e.stopAutoLoad = make(chan struct{}, 1) + e.autoLoadRunning = 0 return e, nil } -// StartAutoLoadPolicy starts a go routine that will every specified duration call LoadPolicy +// 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 } - - // error intentionally ignored - 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) error { - e.watcher = watcher - return watcher.SetUpdateCallback(func(string) { e.LoadPolicy() }) + 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. @@ -84,22 +144,46 @@ 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() error { - e.m.RLock() - defer e.m.RUnlock() + e.m.Lock() + defer e.m.Unlock() return e.Enforcer.BuildRoleLinks() } @@ -110,69 +194,174 @@ func (e *SyncedEnforcer) Enforce(rvals ...interface{}) (bool, error) { 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. @@ -182,6 +371,51 @@ func (e *SyncedEnforcer) AddPolicy(params ...interface{}) (bool, error) { return e.Enforcer.AddPolicy(params...) } +// AddPolicies adds authorization rules to the current policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding rule by adding the new rule. +func (e *SyncedEnforcer) AddPolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPolicies(rules) +} + +// AddPoliciesEx adds authorization rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddPoliciesEx(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPoliciesEx(rules) +} + +// AddNamedPolicy adds an authorization rule to the current named policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *SyncedEnforcer) AddNamedPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedPolicy(ptype, params...) +} + +// AddNamedPolicies adds authorization rules to the current named policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding by adding the new rule. +func (e *SyncedEnforcer) AddNamedPolicies(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedPolicies(ptype, rules) +} + +// AddNamedPoliciesEx adds authorization rules to the current named policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddNamedPoliciesEx(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedPoliciesEx(ptype, rules) +} + // RemovePolicy removes an authorization rule from the current policy. func (e *SyncedEnforcer) RemovePolicy(params ...interface{}) (bool, error) { e.m.Lock() @@ -189,6 +423,51 @@ func (e *SyncedEnforcer) RemovePolicy(params ...interface{}) (bool, error) { return e.Enforcer.RemovePolicy(params...) } +// UpdatePolicy updates an authorization rule from the current policy. +func (e *SyncedEnforcer) UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdatePolicy(oldPolicy, newPolicy) +} + +func (e *SyncedEnforcer) UpdateNamedPolicy(ptype string, p1 []string, p2 []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedPolicy(ptype, p1, p2) +} + +// UpdatePolicies updates authorization rules from the current policies. +func (e *SyncedEnforcer) UpdatePolicies(oldPolices [][]string, newPolicies [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdatePolicies(oldPolices, newPolicies) +} + +func (e *SyncedEnforcer) UpdateNamedPolicies(ptype string, p1 [][]string, p2 [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedPolicies(ptype, p1, p2) +} + +func (e *SyncedEnforcer) UpdateFilteredPolicies(newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateFilteredPolicies(newPolicies, fieldIndex, fieldValues...) +} + +func (e *SyncedEnforcer) UpdateFilteredNamedPolicies(ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateFilteredNamedPolicies(ptype, newPolicies, fieldIndex, fieldValues...) +} + +// RemovePolicies removes authorization rules from the current policy. +func (e *SyncedEnforcer) RemovePolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemovePolicies(rules) +} + // RemoveFilteredPolicy removes an authorization rule from the current policy, field filters can be specified. func (e *SyncedEnforcer) RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error) { e.m.Lock() @@ -196,13 +475,41 @@ func (e *SyncedEnforcer) RemoveFilteredPolicy(fieldIndex int, fieldValues ...str 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. @@ -212,6 +519,51 @@ func (e *SyncedEnforcer) AddGroupingPolicy(params ...interface{}) (bool, error) return e.Enforcer.AddGroupingPolicy(params...) } +// AddGroupingPolicies adds role inheritance rulea to the current policy. +// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. +// Otherwise the function returns true for the corresponding policy rule by adding the new rule. +func (e *SyncedEnforcer) AddGroupingPolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddGroupingPolicies(rules) +} + +// AddGroupingPoliciesEx adds role inheritance rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddGroupingPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddGroupingPoliciesEx(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddGroupingPoliciesEx(rules) +} + +// AddNamedGroupingPolicy adds a named role inheritance rule to the current policy. +// If the rule already exists, the function returns false and the rule will not be added. +// Otherwise the function returns true by adding the new rule. +func (e *SyncedEnforcer) AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedGroupingPolicy(ptype, params...) +} + +// AddNamedGroupingPolicies adds named role inheritance rules to the current policy. +// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. +// Otherwise the function returns true for the corresponding policy rule by adding the new rule. +func (e *SyncedEnforcer) AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedGroupingPolicies(ptype, rules) +} + +// AddNamedGroupingPoliciesEx adds named role inheritance rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedGroupingPolicies, other non-existent rules are added instead of returning false directly. +func (e *SyncedEnforcer) AddNamedGroupingPoliciesEx(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddNamedGroupingPoliciesEx(ptype, rules) +} + // RemoveGroupingPolicy removes a role inheritance rule from the current policy. func (e *SyncedEnforcer) RemoveGroupingPolicy(params ...interface{}) (bool, error) { e.m.Lock() @@ -219,9 +571,116 @@ func (e *SyncedEnforcer) RemoveGroupingPolicy(params ...interface{}) (bool, erro return e.Enforcer.RemoveGroupingPolicy(params...) } +// RemoveGroupingPolicies removes role inheritance rules from the current policy. +func (e *SyncedEnforcer) RemoveGroupingPolicies(rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveGroupingPolicies(rules) +} + // RemoveFilteredGroupingPolicy removes a role inheritance rule from the current policy, field filters can be specified. func (e *SyncedEnforcer) RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error) { e.m.Lock() defer e.m.Unlock() return e.Enforcer.RemoveFilteredGroupingPolicy(fieldIndex, fieldValues...) } + +// RemoveNamedGroupingPolicy removes a role inheritance rule from the current named policy. +func (e *SyncedEnforcer) RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveNamedGroupingPolicy(ptype, params...) +} + +// RemoveNamedGroupingPolicies removes role inheritance rules from the current named policy. +func (e *SyncedEnforcer) RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveNamedGroupingPolicies(ptype, rules) +} + +func (e *SyncedEnforcer) UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateGroupingPolicy(oldRule, newRule) +} + +func (e *SyncedEnforcer) UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateGroupingPolicies(oldRules, newRules) +} + +func (e *SyncedEnforcer) UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedGroupingPolicy(ptype, oldRule, newRule) +} + +func (e *SyncedEnforcer) UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.UpdateNamedGroupingPolicies(ptype, oldRules, newRules) +} + +// RemoveFilteredNamedGroupingPolicy removes a role inheritance rule from the current named policy, field filters can be specified. +func (e *SyncedEnforcer) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.RemoveFilteredNamedGroupingPolicy(ptype, fieldIndex, fieldValues...) +} + +// AddFunction adds a customized function. +func (e *SyncedEnforcer) AddFunction(name string, function govaluate.ExpressionFunction) { + e.m.Lock() + defer e.m.Unlock() + e.Enforcer.AddFunction(name, function) +} + +func (e *SyncedEnforcer) SelfAddPolicy(sec string, ptype string, rule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfAddPolicy(sec, ptype, rule) +} + +func (e *SyncedEnforcer) SelfAddPolicies(sec string, ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfAddPolicies(sec, ptype, rules) +} + +func (e *SyncedEnforcer) SelfAddPoliciesEx(sec string, ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfAddPoliciesEx(sec, ptype, rules) +} + +func (e *SyncedEnforcer) SelfRemovePolicy(sec string, ptype string, rule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfRemovePolicy(sec, ptype, rule) +} + +func (e *SyncedEnforcer) SelfRemovePolicies(sec string, ptype string, rules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfRemovePolicies(sec, ptype, rules) +} + +func (e *SyncedEnforcer) SelfRemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) +} + +func (e *SyncedEnforcer) SelfUpdatePolicy(sec string, ptype string, oldRule, newRule []string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfUpdatePolicy(sec, ptype, oldRule, newRule) +} + +func (e *SyncedEnforcer) SelfUpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.SelfUpdatePolicies(sec, ptype, oldRules, newRules) +} diff --git a/enforcer_synced_test.go b/enforcer_synced_test.go index 19d18ec6f..100f01558 100644 --- a/enforcer_synced_test.go +++ b/enforcer_synced_test.go @@ -15,8 +15,12 @@ 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) { @@ -40,6 +44,490 @@ func TestSync(t *testing.T) { 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 6c7db86d8..bc8f8b4be 100644 --- a/enforcer_test.go +++ b/enforcer_test.go @@ -15,10 +15,14 @@ package casbin import ( + "strings" + "sync" "testing" - "github.com/casbin/casbin/v2/model" - "github.com/casbin/casbin/v2/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) { @@ -55,7 +59,7 @@ func TestKeyMatchModelInMemory(t *testing.T) { testEnforce(t, e, "cathy", "/cathy_data", "DELETE", false) e, _ = NewEnforcer(m) - a.LoadPolicy(e.GetModel()) + _ = a.LoadPolicy(e.GetModel()) testEnforce(t, e, "alice", "/alice_data/resource1", "GET", true) testEnforce(t, e, "alice", "/alice_data/resource1", "POST", true) @@ -80,6 +84,11 @@ 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 := model.NewModel() m.AddDef("r", "r", "sub, obj, act") @@ -104,7 +113,7 @@ func TestRBACModelInMemoryIndeterminate(t *testing.T) { e, _ := NewEnforcer(m) - e.AddPermissionForUser("alice", "data1", "invalid") + _, _ = e.AddPermissionForUser("alice", "data1", "invalid") testEnforce(t, e, "alice", "data1", "read", false) } @@ -119,11 +128,11 @@ func TestRBACModelInMemory(t *testing.T) { 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) @@ -160,11 +169,11 @@ m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act e, _ := NewEnforcer(m) - e.AddPermissionForUser("alice", "data1", "read") - e.AddPermissionForUser("bob", "data2", "write") - e.AddPermissionForUser("data2_admin", "data2", "read") - e.AddPermissionForUser("data2_admin", "data2", "write") - e.AddRoleForUser("alice", "data2_admin") + _, _ = 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) @@ -186,8 +195,8 @@ func TestNotUsedRBACModelInMemory(t *testing.T) { 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) @@ -202,7 +211,19 @@ func TestNotUsedRBACModelInMemory(t *testing.T) { func TestMatcherUsingInOperator(t *testing.T) { // From file config e, _ := NewEnforcer("examples/rbac_model_matcher_using_in_op.conf") - e.AddPermissionForUser("alice", "data1", "read") + _, _ = 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) @@ -215,14 +236,14 @@ func TestMatcherUsingInOperator(t *testing.T) { func TestReloadPolicy(t *testing.T) { 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.SavePolicy() + _ = e.SavePolicy() } func TestClearPolicy(t *testing.T) { @@ -256,21 +277,10 @@ 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") - - 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) + // 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") - // 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) @@ -287,9 +297,9 @@ func TestEnableAutoSave(t *testing.T) { 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) @@ -302,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) @@ -335,8 +345,32 @@ func TestInitWithAdapter(t *testing.T) { func TestRoleLinks(t *testing.T) { e, _ := NewEnforcer("examples/rbac_model.conf") e.EnableAutoBuildRoleLinks(false) - e.BuildRoleLinks() - e.Enforce("user501", "data9", "read") + _ = e.BuildRoleLinks() + _, _ = e.Enforce("user501", "data9", "read") +} + +func TestEnforceConcurrency(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Enforce is not concurrent") + } + }() + + e, _ := NewEnforcer("examples/rbac_model.conf") + _ = e.LoadModel() + + var wg sync.WaitGroup + + // Simulate concurrency (maybe use a timer?) + for i := 1; i <= 10000; i++ { + wg.Add(1) + go func() { + _, _ = e.Enforce("user501", "data9", "read") + wg.Done() + }() + } + + wg.Wait() } func TestGetAndSetModel(t *testing.T) { @@ -359,7 +393,7 @@ func TestGetAndSetAdapterInMem(t *testing.T) { a2 := e2.GetAdapter() e.SetAdapter(a2) - e.LoadPolicy() + _ = e.LoadPolicy() testEnforce(t, e, "alice", "data1", "read", false) testEnforce(t, e, "alice", "data1", "write", true) @@ -372,7 +406,7 @@ func TestSetAdapterFromFile(t *testing.T) { a := fileadapter.NewAdapter("examples/basic_policy.csv") e.SetAdapter(a) - e.LoadPolicy() + _ = e.LoadPolicy() testEnforce(t, e, "alice", "data1", "read", true) } @@ -390,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 8e44fea8b..fa0e9f777 100644 --- a/error_test.go +++ b/error_test.go @@ -17,7 +17,7 @@ package casbin import ( "testing" - "github.com/casbin/casbin/v2/persist/file-adapter" + fileadapter "github.com/casbin/casbin/v3/persist/file-adapter" ) func TestPathError(t *testing.T) { @@ -58,7 +58,7 @@ func TestModelError(t *testing.T) { } } -//func TestPolicyError(t *testing.T) { +// 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.") @@ -70,7 +70,6 @@ func TestModelError(t *testing.T) { 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.") @@ -78,6 +77,15 @@ func TestEnforceError(t *testing.T) { t.Log("Test on error: ") t.Log(err.Error()) } + + e, _ = NewEnforcer("examples/abac_rule_model.conf") + _, err = e.Enforce("wang", "wang", "wang") + if err == nil { + t.Errorf("Should be error here.") + } else { + t.Log("Test on error: ") + t.Log(err.Error()) + } } func TestNoError(t *testing.T) { @@ -125,7 +133,25 @@ func TestMockAdapterErrors(t *testing.T) { e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", adapter) - _, err := e.AddPolicy("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 { + t.Log("Test on error: ") + t.Log(err.Error()) + } + + rules := [][]string{ + {"admin", "domain4", "data1", "read"}, + } + added, err = e.AddPolicies(rules) + if added { + t.Errorf("added should be false") + } if err == nil { t.Errorf("Should be an error here.") @@ -134,7 +160,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err.Error()) } - _, err2 := e.RemoveFilteredPolicy(1, "domain1", "data1") + 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.") @@ -143,7 +172,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err2.Error()) } - _, err3 := e.RemovePolicy("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.") @@ -152,7 +184,25 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err3.Error()) } - _, err4 := e.AddGroupingPolicy("bob", "admin2") + 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.") @@ -161,7 +211,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err4.Error()) } - _, err5 := e.AddNamedGroupingPolicy("g", []string{"eve", "admin2", "domain1"}) + 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.") @@ -170,7 +223,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err5.Error()) } - _, err6 := e.AddNamedPolicy("p", []string{"admin2", "domain2", "data2", "write"}) + 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.") @@ -179,7 +235,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err6.Error()) } - _, err7 := e.RemoveGroupingPolicy("bob", "admin2") + removed, err7 := e.RemoveGroupingPolicy("bob", "admin2") + if removed { + t.Errorf("removed should be false") + } if err7 == nil { t.Errorf("Should be an error here.") @@ -188,7 +247,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err7.Error()) } - _, err8 := e.RemoveFilteredGroupingPolicy(0, "bob") + removed, err8 := e.RemoveFilteredGroupingPolicy(0, "bob") + if removed { + t.Errorf("removed should be false") + } if err8 == nil { t.Errorf("Should be an error here.") @@ -197,7 +259,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err8.Error()) } - _, err9 := e.RemoveNamedGroupingPolicy("g", []string{"alice", "admin", "domain1"}) + 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.") @@ -206,7 +271,10 @@ func TestMockAdapterErrors(t *testing.T) { t.Log(err9.Error()) } - _, err10 := e.RemoveFilteredNamedGroupingPolicy("g", 0, "eve") + 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.") 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 index 68b1450ce..2f358b372 100644 --- a/errors/rbac_errors.go +++ b/errors/rbac_errors.go @@ -16,9 +16,15 @@ package errors import "errors" -// Global errors for rbac defined here +// Global errors for rbac defined here. var ( - ERR_NAME_NOT_FOUND = errors.New("error: name does not exist") - ERR_DOMAIN_PARAMETER = errors.New("error: domain should be 1 parameter") - ERR_NAMES12_NOT_FOUND = errors.New("error: name1 or name2 does not exist") + 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_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_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_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_pattern_policy.csv b/examples/rbac_with_pattern_policy.csv index eff87b62e..bbe76872a 100644 --- a/examples/rbac_with_pattern_policy.csv +++ b/examples/rbac_with_pattern_policy.csv @@ -2,11 +2,27 @@ 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 \ No newline at end of file +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_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 3a3c5f6ea..0b97d522f 100644 --- a/filter_test.go +++ b/filter_test.go @@ -17,14 +17,15 @@ package casbin import ( "testing" - "github.com/casbin/casbin/v2/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) + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) // policy should not be loaded yet testHasPolicy(t, e, []string{"admin", "domain1", "data1", "read"}, false) @@ -34,7 +35,7 @@ func TestLoadFilteredPolicy(t *testing.T) { e, _ := NewEnforcer() adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv") - e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) if err := e.LoadPolicy(); err != nil { t.Errorf("unexpected error in LoadPolicy: %v", err) } @@ -65,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() 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") @@ -80,7 +161,7 @@ func TestFilteredPolicyEmptyFilter(t *testing.T) { e, _ := NewEnforcer() adapter := fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv") - e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + _ = e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) if err := e.LoadFilteredPolicy(nil); err != nil { t.Errorf("unexpected error in LoadFilteredPolicy: %v", err) @@ -109,7 +190,7 @@ func TestFilteredAdapterEmptyFilepath(t *testing.T) { 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("unexpected error in LoadFilteredPolicy: %v", err) @@ -120,7 +201,7 @@ func TestFilteredAdapterInvalidFilepath(t *testing.T) { e, _ := NewEnforcer() adapter := fileadapter.NewFilteredAdapter("examples/does_not_exist_policy.csv") - e.InitWithAdapter("examples/rbac_with_domains_model.conf", adapter) + _ = 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/log/log_util.go b/frontend_old.go similarity index 53% rename from log/log_util.go rename to frontend_old.go index ef4b8165a..139b164fb 100644 --- a/log/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,26 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package log +package casbin -var logger Logger = &DefaultLogger{} +import "encoding/json" -// SetLogger sets the current logger. -func SetLogger(l Logger) { - logger = l -} - -// GetLogger returns the current logger. -func GetLogger() Logger { - return logger -} - -// LogPrint prints the log. -func LogPrint(v ...interface{}) { - logger.Print(v...) -} - -// LogPrintf prints the log with the format. -func LogPrintf(format string, v ...interface{}) { - logger.Printf(format, v...) +func CasbinJsGetPermissionForUserOld(e IEnforcer, user string) ([]byte, error) { + policy, err := e.GetImplicitPermissionsForUser(user) + if err != nil { + return nil, err + } + permission := make(map[string][]string) + for i := 0; i < len(policy); i++ { + permission[policy[i][2]] = append(permission[policy[i][2]], policy[i][1]) + } + b, _ := json.Marshal(permission) + return b, nil } diff --git a/frontend_old_test.go b/frontend_old_test.go new file mode 100644 index 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 index 0c77c23b5..c46f727d3 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ -module github.com/casbin/casbin/v2 +module github.com/casbin/casbin/v3 -require github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible +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 index d1c7c3524..2f3a1c775 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +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 4edbd17dd..7fd16323c 100644 --- a/internal_api.go +++ b/internal_api.go @@ -14,79 +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, error) { - ruleAdded := e.model.AddPolicy(sec, ptype, rule) - if !ruleAdded { - return ruleAdded, nil +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}) } - if e.adapter != nil && e.autoSave { - if err := e.adapter.AddPolicy(sec, ptype, rule); err != nil { + 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 ruleAdded, err + 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.watcher != nil { - err := e.watcher.Update() - if err != nil { - return ruleAdded, 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, nil + 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, error) { - ruleRemoved := e.model.RemovePolicy(sec, ptype, rule) - if !ruleRemoved { - return ruleRemoved, nil +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.adapter != nil && e.autoSave { + if e.shouldPersist() { if err := e.adapter.RemovePolicy(sec, ptype, rule); err != nil { if err.Error() != notImplemented { - return ruleRemoved, err + return false, err } } - if e.watcher != nil { - err := e.watcher.Update() - if err != nil { - return ruleRemoved, err - } + } + + ruleRemoved, err := e.model.RemovePolicy(sec, ptype, rule) + if !ruleRemoved || err != nil { + return ruleRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{rule}) + if err != nil { + return ruleRemoved, err + } + + // Validate constraints after removing grouping policy + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err } } return ruleRemoved, nil } +func (e *Enforcer) updatePolicyWithoutNotify(sec string, ptype string, oldRule []string, newRule []string) (bool, error) { + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.UpdatePolicy(sec, ptype, oldRule, newRule) + } + + if e.shouldPersist() { + if err := e.adapter.(persist.UpdatableAdapter).UpdatePolicy(sec, ptype, oldRule, newRule); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + ruleUpdated, err := e.model.UpdatePolicy(sec, ptype, oldRule, newRule) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{oldRule}) // remove the old rule + if err != nil { + return ruleUpdated, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{newRule}) // add the new rule + if err != nil { + return ruleUpdated, err + } + + // Validate constraints after updating grouping policy + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return ruleUpdated, nil +} + +func (e *Enforcer) updatePoliciesWithoutNotify(sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + if len(newRules) != len(oldRules) { + return false, fmt.Errorf("the length of oldRules should be equal to the length of newRules, but got the length of oldRules is %d, the length of newRules is %d", len(oldRules), len(newRules)) + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.UpdatePolicies(sec, ptype, oldRules, newRules) + } + + if e.shouldPersist() { + if err := e.adapter.(persist.UpdatableAdapter).UpdatePolicies(sec, ptype, oldRules, newRules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + ruleUpdated, err := e.model.UpdatePolicies(sec, ptype, oldRules, newRules) + if !ruleUpdated || err != nil { + return ruleUpdated, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules + if err != nil { + return ruleUpdated, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules + if err != nil { + return ruleUpdated, err + } + + // Validate constraints after updating grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + + return ruleUpdated, nil +} + +// removePolicies removes rules from the current policy. +func (e *Enforcer) removePoliciesWithoutNotify(sec string, ptype string, rules [][]string) (bool, error) { + if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); !hasPolicies || err != nil { + return hasPolicies, err + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return true, e.dispatcher.RemovePolicies(sec, ptype, rules) + } + + if e.shouldPersist() { + if err := e.adapter.(persist.BatchAdapter).RemovePolicies(sec, ptype, rules); err != nil { + if err.Error() != notImplemented { + return false, err + } + } + } + + rulesRemoved, err := e.model.RemovePolicies(sec, ptype, rules) + if !rulesRemoved || err != nil { + return rulesRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, rules) + if err != nil { + return rulesRemoved, err + } + + // Validate constraints after removing grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err + } + } + return rulesRemoved, nil +} + // removeFilteredPolicy removes rules based on field filters from the current policy. -func (e *Enforcer) removeFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) { - ruleRemoved := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) - if !ruleRemoved { - return ruleRemoved, nil +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.adapter != nil && e.autoSave { + if e.shouldPersist() { if err := e.adapter.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...); err != nil { if err.Error() != notImplemented { - return ruleRemoved, err + return false, err } } - if e.watcher != nil { - err := e.watcher.Update() - if err != nil { - return ruleRemoved, err - } + } + + ruleRemoved, effects, err := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + if !ruleRemoved || err != nil { + return ruleRemoved, err + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, effects) + if err != nil { + return ruleRemoved, err + } + + // Validate constraints after removing filtered grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return false, err } } return ruleRemoved, nil } + +func (e *Enforcer) updateFilteredPoliciesWithoutNotify(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { + var ( + oldRules [][]string + err error + ) + + if _, err = e.model.GetAssertion(sec, ptype); err != nil { + return oldRules, err + } + + if e.shouldPersist() { + if oldRules, err = e.adapter.(persist.UpdatableAdapter).UpdateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues...); err != nil { + if err.Error() != notImplemented { + return nil, err + } + } + // For compatibility, because some adapters return oldRules containing ptype, see https://github.com/casbin/xorm-adapter/issues/49 + for i, oldRule := range oldRules { + if len(oldRules[i]) == len(e.model[sec][ptype].Tokens)+1 { + oldRules[i] = oldRule[1:] + } + } + } + + if e.dispatcher != nil && e.autoNotifyDispatcher { + return oldRules, e.dispatcher.UpdateFilteredPolicies(sec, ptype, oldRules, newRules) + } + + ruleChanged, err := e.model.RemovePolicies(sec, ptype, oldRules) + if err != nil { + return oldRules, err + } + err = e.model.AddPolicies(sec, ptype, newRules) + if err != nil { + return oldRules, err + } + ruleChanged = ruleChanged && len(newRules) != 0 + if !ruleChanged { + return make([][]string, 0), nil + } + + if sec == "g" { + err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules + if err != nil { + return oldRules, err + } + err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules + if err != nil { + return oldRules, err + } + + // Validate constraints after updating filtered grouping policies + if err := e.validateConstraintsForGroupingPolicy(); err != nil { + return oldRules, err + } + } + + return oldRules, nil +} + +// addPolicy adds a rule to the current policy. +func (e *Enforcer) addPolicy(sec string, ptype string, rule []string) (bool, error) { + ok, err := e.logPolicyOperation(log.EventAddPolicy, sec, rule, func() (bool, error) { + return e.addPolicyWithoutNotify(sec, ptype, rule) + }) + + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var notifyErr error + if watcher, isWatcherEx := e.watcher.(persist.WatcherEx); isWatcherEx { + notifyErr = watcher.UpdateForAddPolicy(sec, ptype, rule...) + } else { + notifyErr = e.watcher.Update() + } + return true, notifyErr + } + + return true, nil +} + +// addPolicies adds rules to the current policy. +// If autoRemoveRepeat == true, existing rules are automatically filtered +// Otherwise, false is returned directly. +func (e *Enforcer) addPolicies(sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) { + ok, err := e.addPoliciesWithoutNotify(sec, ptype, rules, autoRemoveRepeat) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForAddPolicies(sec, ptype, rules...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +// removePolicy removes a rule from the current policy. +func (e *Enforcer) removePolicy(sec string, ptype string, rule []string) (bool, error) { + ok, err := e.logPolicyOperation(log.EventRemovePolicy, sec, rule, func() (bool, error) { + return e.removePolicyWithoutNotify(sec, ptype, rule) + }) + + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var notifyErr error + if watcher, isWatcherEx := e.watcher.(persist.WatcherEx); isWatcherEx { + notifyErr = watcher.UpdateForRemovePolicy(sec, ptype, rule...) + } else { + notifyErr = e.watcher.Update() + } + return true, notifyErr + } + + return true, nil +} + +func (e *Enforcer) updatePolicy(sec string, ptype string, oldRule []string, newRule []string) (bool, error) { + ok, err := e.updatePolicyWithoutNotify(sec, ptype, oldRule, newRule) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicy(sec, ptype, oldRule, newRule) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *Enforcer) updatePolicies(sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + ok, err := e.updatePoliciesWithoutNotify(sec, ptype, oldRules, newRules) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +// removePolicies removes rules from the current policy. +func (e *Enforcer) removePolicies(sec string, ptype string, rules [][]string) (bool, error) { + ok, err := e.removePoliciesWithoutNotify(sec, ptype, rules) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForRemovePolicies(sec, ptype, rules...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +// removeFilteredPolicy removes rules based on field filters from the current policy. +func (e *Enforcer) removeFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) { + ok, err := e.removeFilteredPolicyWithoutNotify(sec, ptype, fieldIndex, fieldValues) + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.WatcherEx); ok { + err = watcher.UpdateForRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *Enforcer) updateFilteredPolicies(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + oldRules, err := e.updateFilteredPoliciesWithoutNotify(sec, ptype, newRules, fieldIndex, fieldValues...) + ok := len(oldRules) != 0 + if !ok || err != nil { + return ok, err + } + + if e.shouldNotify() { + var err error + if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok { + err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules) + } else { + err = e.watcher.Update() + } + return true, err + } + + return true, nil +} + +func (e *Enforcer) GetFieldIndex(ptype string, field string) (int, error) { + return e.model.GetFieldIndex(ptype, field) +} + +func (e *Enforcer) SetFieldIndex(ptype string, field string, index int) { + assertion := e.model["p"][ptype] + assertion.FieldIndexMutex.Lock() + assertion.FieldIndexMap[field] = index + assertion.FieldIndexMutex.Unlock() +} diff --git a/lbac_test.go b/lbac_test.go new file mode 100644 index 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 index 74a042281..13ec21d99 100644 --- a/log/default_logger.go +++ b/log/default_logger.go @@ -1,4 +1,4 @@ -// Copyright 2018 The casbin Authors. All Rights Reserved. +// 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. @@ -14,29 +14,126 @@ package log -import "log" +import ( + "fmt" + "io" + "os" + "strings" + "time" +) -// DefaultLogger is the implementation for a Logger using golang log. +// DefaultLogger is the default implementation of the Logger interface. type DefaultLogger struct { - enable bool + output io.Writer + eventTypes map[EventType]bool + logCallback func(entry *LogEntry) error } -func (l *DefaultLogger) EnableLog(enable bool) { - l.enable = enable +// 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 } -func (l *DefaultLogger) IsEnabled() bool { - return l.enable +// 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 } -func (l *DefaultLogger) Print(v ...interface{}) { - if l.enable { - log.Print(v...) +// 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 } -func (l *DefaultLogger) Printf(format string, v ...interface{}) { - if l.enable { - log.Printf(format, v...) +// 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/log_util_test.go b/log/log_util_test.go deleted file mode 100644 index 72cd56ba7..000000000 --- a/log/log_util_test.go +++ /dev/null @@ -1,53 +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 log - -import ( - "reflect" - "testing" -) - -type LoggerTester struct { - format string - lastMessage []interface{} -} - -func (t *LoggerTester) EnableLog(bool) {} -func (t *LoggerTester) IsEnabled() bool { return true } - -func (t *LoggerTester) Print(v ...interface{}) { - t.format = "" - t.lastMessage = v -} - -func (t *LoggerTester) Printf(format string, v ...interface{}) { - t.format = format - t.lastMessage = v -} - -func TestLog(t *testing.T) { - lt := &LoggerTester{} - SetLogger(lt) - - LogPrint(1, "1", true) - if lt.format != "" || !reflect.DeepEqual(lt.lastMessage, []interface{}{1, "1", true}) { - t.Errorf("incorrect logger message: %+v", lt.lastMessage) - } - - LogPrintf("%d", 2, "2", false) - if lt.format != "%d" || !reflect.DeepEqual(lt.lastMessage, []interface{}{2, "2", false}) { - t.Errorf("incorrect logger message: %+v", lt.lastMessage) - } -} diff --git a/log/logger.go b/log/logger.go index e3aadd1a5..87153fdf2 100644 --- a/log/logger.go +++ b/log/logger.go @@ -1,4 +1,4 @@ -// Copyright 2018 The casbin Authors. All Rights Reserved. +// 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. @@ -14,17 +14,13 @@ package log -// Logger is the logging interface implementation. +// Logger defines the interface for event-driven logging in Casbin. type Logger interface { - //EnableLog controls whether print the message. - EnableLog(bool) + 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 - //IsEnabled returns if logger is enabled. - IsEnabled() bool - - //Print formats using the default formats for its operands and logs the message. - Print(...interface{}) - - //Printf formats according to a format specifier and logs the message. - Printf(string, ...interface{}) + 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 19b84095e..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) } @@ -120,20 +237,48 @@ func (e *Enforcer) AddPolicy(params ...interface{}) (bool, error) { return e.AddNamedPolicy("p", params...) } +// AddPolicies adds authorization rules to the current policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding rule by adding the new rule. +func (e *Enforcer) AddPolicies(rules [][]string) (bool, error) { + return e.AddNamedPolicies("p", rules) +} + +// AddPoliciesEx adds authorization rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddPolicies, other non-existent rules are added instead of returning false directly. +func (e *Enforcer) AddPoliciesEx(rules [][]string) (bool, error) { + return e.AddNamedPoliciesEx("p", rules) +} + // AddNamedPolicy adds an authorization rule to the current named policy. // If the rule already exists, the function returns false and the rule will not be added. // Otherwise the function returns true by adding the new rule. func (e *Enforcer) AddNamedPolicy(ptype string, params ...interface{}) (bool, error) { if strSlice, ok := params[0].([]string); len(params) == 1 && ok { + strSlice = append(make([]string, 0, len(strSlice)), strSlice...) return e.addPolicy("p", ptype, strSlice) - } else { - policy := make([]string, 0) - for _, param := range params { - policy = append(policy, param.(string)) - } - - return e.addPolicy("p", ptype, policy) } + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.addPolicy("p", ptype, policy) +} + +// AddNamedPolicies adds authorization rules to the current named policy. +// If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. +// Otherwise the function returns true for the corresponding by adding the new rule. +func (e *Enforcer) AddNamedPolicies(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("p", ptype, rules, false) +} + +// AddNamedPoliciesEx adds authorization rules to the current named policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedPolicies, other non-existent rules are added instead of returning false directly. +func (e *Enforcer) AddNamedPoliciesEx(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("p", ptype, rules, true) } // RemovePolicy removes an authorization rule from the current policy. @@ -141,6 +286,37 @@ func (e *Enforcer) RemovePolicy(params ...interface{}) (bool, error) { return e.RemoveNamedPolicy("p", params...) } +// UpdatePolicy updates an authorization rule from the current policy. +func (e *Enforcer) UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error) { + return e.UpdateNamedPolicy("p", oldPolicy, newPolicy) +} + +func (e *Enforcer) UpdateNamedPolicy(ptype string, p1 []string, p2 []string) (bool, error) { + return e.updatePolicy("p", ptype, p1, p2) +} + +// UpdatePolicies updates authorization rules from the current policies. +func (e *Enforcer) UpdatePolicies(oldPolices [][]string, newPolicies [][]string) (bool, error) { + return e.UpdateNamedPolicies("p", oldPolices, newPolicies) +} + +func (e *Enforcer) UpdateNamedPolicies(ptype string, p1 [][]string, p2 [][]string) (bool, error) { + return e.updatePolicies("p", ptype, p1, p2) +} + +func (e *Enforcer) UpdateFilteredPolicies(newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.UpdateFilteredNamedPolicies("p", newPolicies, fieldIndex, fieldValues...) +} + +func (e *Enforcer) UpdateFilteredNamedPolicies(ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) { + return e.updateFilteredPolicies("p", ptype, newPolicies, fieldIndex, fieldValues...) +} + +// RemovePolicies removes authorization rules from the current policy. +func (e *Enforcer) RemovePolicies(rules [][]string) (bool, error) { + return e.RemoveNamedPolicies("p", rules) +} + // RemoveFilteredPolicy removes an authorization rule from the current policy, field filters can be specified. func (e *Enforcer) RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) (bool, error) { return e.RemoveFilteredNamedPolicy("p", fieldIndex, fieldValues...) @@ -150,28 +326,32 @@ func (e *Enforcer) RemoveFilteredPolicy(fieldIndex int, fieldValues ...string) ( func (e *Enforcer) RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error) { if strSlice, ok := params[0].([]string); len(params) == 1 && ok { return e.removePolicy("p", ptype, strSlice) - } else { - policy := make([]string, 0) - for _, param := range params { - policy = append(policy, param.(string)) - } - - return e.removePolicy("p", ptype, policy) } + policy := make([]string, 0) + for _, param := range params { + policy = append(policy, param.(string)) + } + + return e.removePolicy("p", ptype, policy) +} + +// RemoveNamedPolicies removes authorization rules from the current named policy. +func (e *Enforcer) RemoveNamedPolicies(ptype string, rules [][]string) (bool, error) { + return e.removePolicies("p", ptype, rules) } // RemoveFilteredNamedPolicy removes an authorization rule from the current named policy, field filters can be specified. func (e *Enforcer) RemoveFilteredNamedPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { - return e.removeFilteredPolicy("p", ptype, fieldIndex, fieldValues...) + 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) } @@ -191,6 +371,20 @@ 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. @@ -208,17 +402,33 @@ func (e *Enforcer) AddNamedGroupingPolicy(ptype string, params ...interface{}) ( ruleAdded, err = e.addPolicy("g", ptype, policy) } - if e.autoBuildRoleLinks { - e.BuildRoleLinks() - } return ruleAdded, err } +// AddNamedGroupingPolicies adds named role inheritance rules to the current policy. +// If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. +// Otherwise the function returns true for the corresponding policy rule by adding the new rule. +func (e *Enforcer) AddNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("g", ptype, rules, false) +} + +// AddNamedGroupingPoliciesEx adds named role inheritance rules to the current policy. +// If the rule already exists, the rule will not be added. +// But unlike AddNamedGroupingPolicies, other non-existent rules are added instead of returning false directly. +func (e *Enforcer) AddNamedGroupingPoliciesEx(ptype string, rules [][]string) (bool, error) { + return e.addPolicies("g", ptype, rules, true) +} + // RemoveGroupingPolicy removes a role inheritance rule from the current policy. func (e *Enforcer) RemoveGroupingPolicy(params ...interface{}) (bool, error) { return e.RemoveNamedGroupingPolicy("g", params...) } +// RemoveGroupingPolicies removes role inheritance rules from the current policy. +func (e *Enforcer) RemoveGroupingPolicies(rules [][]string) (bool, error) { + return e.RemoveNamedGroupingPolicies("g", rules) +} + // RemoveFilteredGroupingPolicy removes a role inheritance rule from the current policy, field filters can be specified. func (e *Enforcer) RemoveFilteredGroupingPolicy(fieldIndex int, fieldValues ...string) (bool, error) { return e.RemoveFilteredNamedGroupingPolicy("g", fieldIndex, fieldValues...) @@ -239,23 +449,69 @@ func (e *Enforcer) RemoveNamedGroupingPolicy(ptype string, params ...interface{} ruleRemoved, err = e.removePolicy("g", ptype, policy) } - if e.autoBuildRoleLinks { - e.BuildRoleLinks() - } return ruleRemoved, err } +// RemoveNamedGroupingPolicies removes role inheritance rules from the current named policy. +func (e *Enforcer) RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error) { + return e.removePolicies("g", ptype, rules) +} + +func (e *Enforcer) UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) { + return e.UpdateNamedGroupingPolicy("g", oldRule, newRule) +} + +// UpdateGroupingPolicies updates authorization rules from the current policies. +func (e *Enforcer) UpdateGroupingPolicies(oldRules [][]string, newRules [][]string) (bool, error) { + return e.UpdateNamedGroupingPolicies("g", oldRules, newRules) +} + +func (e *Enforcer) UpdateNamedGroupingPolicy(ptype string, oldRule []string, newRule []string) (bool, error) { + return e.updatePolicy("g", ptype, oldRule, newRule) +} + +func (e *Enforcer) UpdateNamedGroupingPolicies(ptype string, oldRules [][]string, newRules [][]string) (bool, error) { + return e.updatePolicies("g", ptype, oldRules, newRules) +} + // RemoveFilteredNamedGroupingPolicy removes a role inheritance rule from the current named policy, field filters can be specified. func (e *Enforcer) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error) { - ruleRemoved, err := e.removeFilteredPolicy("g", ptype, fieldIndex, fieldValues...) - - if e.autoBuildRoleLinks { - e.BuildRoleLinks() - } - return ruleRemoved, err + return e.removeFilteredPolicy("g", ptype, fieldIndex, fieldValues) } // AddFunction adds a customized function. -func (e *Enforcer) AddFunction(name string, function func(args ...interface{}) (interface{}, error)) { +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 94b8fc94c..620b39447 100644 --- a/management_api_test.go +++ b/management_api_test.go @@ -17,12 +17,16 @@ package casbin import ( "testing" - "github.com/casbin/casbin/v2/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() + myRes, err := f() + if err != nil { + t.Error(err) + } + t.Log(title+": ", myRes) if !util.ArrayEquals(res, myRes) { @@ -37,21 +41,40 @@ func TestGetList(t *testing.T) { 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() + myRes, err := e.GetPolicy() + if err != nil { + t.Error(err) + } + t.Log("Policy: ", myRes) - if !util.Array2DEquals(res, 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...) + 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) { @@ -59,9 +82,27 @@ func testGetFilteredPolicy(t *testing.T, e *Enforcer, fieldIndex int, res [][]st } } +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() + myRes, err := e.GetGroupingPolicy() + if err != nil { + t.Error(err) + } + t.Log("Grouping policy: ", myRes) if !util.Array2DEquals(res, myRes) { @@ -71,7 +112,11 @@ 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...) + 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) { @@ -81,7 +126,11 @@ 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) + myRes, err := e.HasPolicy(policy) + if err != nil { + t.Error(err) + } + t.Log("Has policy ", util.ArrayToString(policy), ": ", myRes) if res != myRes { @@ -91,7 +140,11 @@ 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) + myRes, err := e.HasGroupingPolicy(policy) + if err != nil { + t.Error(err) + } + t.Log("Has grouping policy ", util.ArrayToString(policy), ": ", myRes) if res != myRes { @@ -116,6 +169,13 @@ func TestGetPolicyAPI(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") @@ -148,61 +208,157 @@ func TestModifyPolicyAPI(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 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 e462cae7c..b5b8e9199 100644 --- a/model/assertion.go +++ b/model/assertion.go @@ -17,50 +17,183 @@ package model import ( "errors" "strings" + "sync" - "github.com/casbin/casbin/v2/log" - "github.com/casbin/casbin/v2/rbac" + "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) error { +func (ast *Assertion) buildIncrementalRoleLinks(rm rbac.RoleManager, op PolicyOp, rules [][]string) error { ast.RM = rm count := strings.Count(ast.Value, "_") - for _, rule := range ast.Policy { - if count < 2 { - return errors.New("the number of \"_\" in role definition should be at least 2") - } + 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 count == 2 { - err := ast.RM.AddLink(rule[0], rule[1]) - if err != nil { - return err - } - } else if count == 3 { - err := ast.RM.AddLink(rule[0], rule[1], rule[2]) + 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 } - } else if count == 4 { - err := ast.RM.AddLink(rule[0], rule[1], rule[2], rule[3]) + case PolicyRemove: + err := rm.DeleteLink(rule[0], rule[1], rule[2:]...) if err != nil { return err } } } + return nil +} + +func (ast *Assertion) buildRoleLinks(rm rbac.RoleManager) error { + ast.RM = rm + count := strings.Count(ast.Value, "_") + if count < 2 { + return errors.New("the number of \"_\" in role definition should be at least 2") + } + for _, rule := range ast.Policy { + if len(rule) < count { + return errors.New("grouping policy elements do not meet role definition") + } + if len(rule) > count { + rule = rule[:count] + } + err := ast.RM.AddLink(rule[0], rule[1], rule[2:]...) + if err != nil { + return err + } + } + + return nil +} + +func (ast *Assertion) buildIncrementalConditionalRoleLinks(condRM rbac.ConditionalRoleManager, op PolicyOp, rules [][]string) error { + ast.CondRM = condRM + count := strings.Count(ast.Value, "_") + if count < 2 { + return errors.New("the number of \"_\" in role definition should be at least 2") + } + + for _, rule := range rules { + if len(rule) < count { + return errors.New("grouping policy elements do not meet role definition") + } + if len(rule) > count { + rule = rule[:count] + } + + var err error + domainRule := rule[2:len(ast.Tokens)] + + switch op { + case PolicyAdd: + err = ast.addConditionalRoleLink(rule, domainRule) + case PolicyRemove: + err = ast.CondRM.DeleteLink(rule[0], rule[1], rule[2:]...) + } + if err != nil { + return err + } + } + + return nil +} + +func (ast *Assertion) buildConditionalRoleLinks(condRM rbac.ConditionalRoleManager) error { + ast.CondRM = condRM + count := strings.Count(ast.Value, "_") + if count < 2 { + return errors.New("the number of \"_\" in role definition should be at least 2") + } + for _, rule := range ast.Policy { + if len(rule) < count { + return errors.New("grouping policy elements do not meet role definition") + } + if len(rule) > count { + rule = rule[:count] + } + + domainRule := rule[2:len(ast.Tokens)] + + err := ast.addConditionalRoleLink(rule, domainRule) + if err != nil { + return err + } + } + + return nil +} + +// addConditionalRoleLink adds Link to rbac.ConditionalRoleManager and sets the parameters for LinkConditionFunc. +func (ast *Assertion) addConditionalRoleLink(rule []string, domainRule []string) error { + var err error + if len(domainRule) == 0 { + err = ast.CondRM.AddLink(rule[0], rule[1]) + if err == nil { + ast.CondRM.SetLinkConditionFuncParams(rule[0], rule[1], rule[len(ast.Tokens):]...) + } + } else { + domain := domainRule[0] + err = ast.CondRM.AddLink(rule[0], rule[1], domain) + if err == nil { + ast.CondRM.SetDomainLinkConditionFuncParams(rule[0], rule[1], domain, rule[len(ast.Tokens):]...) + } + } + return err +} + +func (ast *Assertion) copy() *Assertion { + tokens := append([]string(nil), ast.Tokens...) + policy := make([][]string, len(ast.Policy)) + + for i, p := range ast.Policy { + policy[i] = append(policy[i], p...) + } + policyMap := make(map[string]int) + for k, v := range ast.PolicyMap { + policyMap[k] = v + } + + ast.FieldIndexMutex.RLock() + fieldIndexMap := make(map[string]int) + for k, v := range ast.FieldIndexMap { + fieldIndexMap[k] = v + } + ast.FieldIndexMutex.RUnlock() + + newAst := &Assertion{ + Key: ast.Key, + Value: ast.Value, + PolicyMap: policyMap, + Tokens: tokens, + Policy: policy, + FieldIndexMap: fieldIndexMap, + ParamsTokens: append([]string(nil), ast.ParamsTokens...), + RM: ast.RM, + CondRM: ast.CondRM, + } - log.LogPrint("Role links for: " + ast.Key) - return 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 1894367d3..956c94b9d 100644 --- a/model/function.go +++ b/model/function.go @@ -14,27 +14,53 @@ package model -import "github.com/casbin/casbin/v2/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 8e8ac8152..b541e1b84 100644 --- a/model/model.go +++ b/model/model.go @@ -15,12 +15,17 @@ package model import ( + "container/list" + "errors" + "fmt" + "regexp" + "sort" "strconv" "strings" - "github.com/casbin/casbin/v2/config" - "github.com/casbin/casbin/v2/log" - "github.com/casbin/casbin/v2/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. @@ -29,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) @@ -92,10 +131,11 @@ func loadSection(model Model, cfg config.ConfigInterface, sec string) { // NewModel creates an empty model. func NewModel() Model { m := make(Model) + return m } -// NewModel creates a model from a .CONF file. +// NewModelFromFile creates a model from a .CONF file. func NewModelFromFile(path string) (Model, error) { m := NewModel() @@ -107,7 +147,7 @@ func NewModelFromFile(path string) (Model, error) { return m, nil } -// NewModel creates a model from a string which contains model text. +// NewModelFromString creates a model from a string which contains model text. func NewModelFromString(text string) (Model, error) { m := NewModel() @@ -126,14 +166,7 @@ func (model Model) LoadModel(path string) error { return err } - loadSection(model, cfg, "r") - loadSection(model, cfg, "p") - loadSection(model, cfg, "e") - loadSection(model, cfg, "m") - - loadSection(model, cfg, "g") - - return nil + return model.loadModelFromConfig(cfg) } // LoadModelFromText loads the model from the text. @@ -143,22 +176,261 @@ func (model Model) LoadModelFromText(text string) error { return err } - loadSection(model, cfg, "r") - loadSection(model, cfg, "p") - loadSection(model, cfg, "e") - loadSection(model, cfg, "m") + return model.loadModelFromConfig(cfg) +} + +// loadModelFromConfig loads the model from a config interface. +// It loads all sections defined in sectionNameMap and validates that required sections are present. +// If constraint_definition section exists, it validates all constraints against the current policy. +// Note: Constraint validation is performed during model loading, which may affect loading performance +// and can cause model loading to fail if constraints are violated or invalid. +func (model Model) loadModelFromConfig(cfg config.ConfigInterface) error { + for s := range sectionNameMap { + loadSection(model, cfg, s) + } + ms := make([]string, 0) + for _, rs := range requiredSections { + if !model.hasSection(rs) { + ms = append(ms, sectionNameMap[rs]) + } + } + if len(ms) > 0 { + return fmt.Errorf("missing required sections: %s", strings.Join(ms, ",")) + } - loadSection(model, cfg, "g") + // 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() { - log.LogPrint("Model:") - for k, v := range model { - for i, j := range v { - log.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 090069d43..e55bf4105 100644 --- a/model/policy.go +++ b/model/policy.go @@ -15,53 +15,113 @@ package model import ( - "github.com/casbin/casbin/v2/log" - "github.com/casbin/casbin/v2/rbac" - "github.com/casbin/casbin/v2/util" + "fmt" + "strconv" + "strings" + + "github.com/casbin/casbin/v3/constant" + "github.com/casbin/casbin/v3/rbac" + "github.com/casbin/casbin/v3/util" ) -// BuildRoleLinks initializes the roles in RBAC. -func (model Model) BuildRoleLinks(rm rbac.RoleManager) error { - for _, ast := range model["g"] { - err := ast.buildRoleLinks(rm) +type ( + PolicyOp int +) + +const ( + PolicyAdd PolicyOp = iota + PolicyRemove +) + +const DefaultSep = "," + +// BuildIncrementalRoleLinks provides incremental build the role inheritance relations. +func (model Model) BuildIncrementalRoleLinks(rmMap map[string]rbac.RoleManager, op PolicyOp, sec string, ptype string, rules [][]string) error { + if sec == "g" && rmMap[ptype] != nil { + _, err := model.GetAssertion(sec, ptype) if err != nil { return err } + return model[sec][ptype].buildIncrementalRoleLinks(rmMap[ptype], op, rules) + } + return nil +} + +// BuildRoleLinks initializes the roles in RBAC. +func (model Model) BuildRoleLinks(rmMap map[string]rbac.RoleManager) error { + model.PrintPolicy() + for ptype, ast := range model["g"] { + if rm := rmMap[ptype]; rm != nil { + err := ast.buildRoleLinks(rm) + if err != nil { + return err + } + } } return nil } -// PrintPolicy prints the policy to log. -func (model Model) PrintPolicy() { - log.LogPrint("Policy:") - for key, ast := range model["p"] { - log.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"] { - log.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 { @@ -78,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 + } + 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 false + 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 { @@ -127,25 +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) - 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 45fec6257..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 { @@ -40,7 +42,7 @@ func BenchmarkBasicModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = e.Enforce("alice", "data1", "read") } } @@ -49,67 +51,163 @@ func BenchmarkRBACModel(b *testing.B) { 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) + // 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) + // 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) + // 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") } } @@ -118,7 +216,7 @@ func BenchmarkRBACModelWithResourceRoles(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = e.Enforce("alice", "data1", "read") } } @@ -127,7 +225,7 @@ func BenchmarkRBACModelWithDomains(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "domain1", "data1", "read") + _, _ = e.Enforce("alice", "domain1", "data1", "read") } } @@ -137,7 +235,21 @@ func BenchmarkABACModel(b *testing.B) { 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") } } @@ -146,7 +258,7 @@ func BenchmarkKeyMatchModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "/alice_data/resource1", "GET") + _, _ = e.Enforce("alice", "/alice_data/resource1", "GET") } } @@ -155,7 +267,7 @@ func BenchmarkRBACModelWithDeny(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = e.Enforce("alice", "data1", "read") } } @@ -164,6 +276,16 @@ func BenchmarkPriorityModel(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - e.Enforce("alice", "data1", "read") + _, _ = e.Enforce("alice", "data1", "read") + } +} + +func BenchmarkRBACModelWithDomainPatternLarge(b *testing.B) { + e, _ := NewEnforcer("examples/performance/rbac_with_pattern_large_scale_model.conf", "examples/performance/rbac_with_pattern_large_scale_policy.csv") + e.AddNamedDomainMatchingFunc("g", "", util.KeyMatch4) + _ = e.BuildRoleLinks() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = e.Enforce("staffUser1001", "/orgs/1/sites/site001", "App001.Module001.Action1001") } } diff --git a/model_test.go b/model_test.go index d02e39540..c3e5432df 100644 --- a/model_test.go +++ b/model_test.go @@ -17,15 +17,17 @@ package casbin import ( "testing" - "github.com/casbin/casbin/v2/persist/file-adapter" - "github.com/casbin/casbin/v2/rbac" - "github.com/casbin/casbin/v2/rbac/default-role-manager" - "github.com/casbin/casbin/v2/util" + "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 myRes, _ := e.Enforce(sub, obj, act); myRes != 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) } } @@ -39,7 +41,9 @@ func testEnforceWithoutUsers(t *testing.T, e *Enforcer, obj string, act string, func testDomainEnforce(t *testing.T, e *Enforcer, sub string, dom string, obj string, act string, res bool) { t.Helper() - if myRes, _ := e.Enforce(sub, dom, obj, act); myRes != 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) } } @@ -57,6 +61,19 @@ func TestBasicModel(t *testing.T) { testEnforce(t, e, "bob", "data2", "write", true) } +func TestBasicModelWithoutSpaces(t *testing.T) { + e, _ := NewEnforcer("examples/basic_model_without_spaces.conf", "examples/basic_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "alice", "data1", "write", false) + testEnforce(t, e, "alice", "data2", "read", false) + testEnforce(t, e, "alice", "data2", "write", false) + testEnforce(t, e, "bob", "data1", "read", false) + testEnforce(t, e, "bob", "data1", "write", false) + testEnforce(t, e, "bob", "data2", "read", false) + testEnforce(t, e, "bob", "data2", "write", true) +} + func TestBasicModelNoPolicy(t *testing.T) { e, _ := NewEnforcer("examples/basic_model.conf") @@ -164,13 +181,13 @@ func TestRBACModelWithDomains(t *testing.T) { func TestRBACModelWithDomainsAtRuntime(t *testing.T) { e, _ := NewEnforcer("examples/rbac_with_domains_model.conf") - e.AddPolicy("admin", "domain1", "data1", "read") - e.AddPolicy("admin", "domain1", "data1", "write") - e.AddPolicy("admin", "domain2", "data2", "read") - e.AddPolicy("admin", "domain2", "data2", "write") + _, _ = e.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) @@ -182,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) @@ -194,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) @@ -210,20 +227,101 @@ func TestRBACModelWithDomainsAtRuntimeMockAdapter(t *testing.T) { adapter := fileadapter.NewAdapterMock("examples/rbac_with_domains_policy.csv") e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", adapter) - e.AddPolicy("admin", "domain3", "data1", "read") - e.AddGroupingPolicy("alice", "admin", "domain3") + _, _ = 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") @@ -249,7 +347,7 @@ func TestRBACModelWithCustomData(t *testing.T) { // 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) @@ -263,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) @@ -283,7 +381,13 @@ func TestRBACModelWithPattern(t *testing.T) { // 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.rm.(*defaultrolemanager.RoleManager).AddMatchingFunc("KeyMatch2", util.KeyMatch2) + 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) @@ -295,7 +399,7 @@ func TestRBACModelWithPattern(t *testing.T) { // 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.rm.(*defaultrolemanager.RoleManager).AddMatchingFunc("KeyMatch3", util.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) @@ -306,6 +410,35 @@ func TestRBACModelWithPattern(t *testing.T) { 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 { @@ -315,6 +448,9 @@ func (rm *testCustomRoleManager) Clear() error { return nil } func (rm *testCustomRoleManager) AddLink(name1 string, name2 string, domain ...string) error { return nil } +func (rm *testCustomRoleManager) BuildRelationship(name1 string, name2 string, domain ...string) error { + return nil +} func (rm *testCustomRoleManager) DeleteLink(name1 string, name2 string, domain ...string) error { return nil } @@ -334,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.SetRoleManager(NewRoleManager()) - e.LoadModel() + _ = e.LoadModel() _ = e.LoadPolicy() testEnforce(t, e, "alice", "data1", "read", true) @@ -352,34 +519,6 @@ 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") @@ -429,7 +568,7 @@ func CustomFunctionWrapper(args ...interface{}) (interface{}, error) { key1 := args[0].(string) key2 := args[1].(string) - return bool(CustomFunction(key1, key2)), nil + return CustomFunction(key1, key2), nil } func TestKeyMatchCustomModel(t *testing.T) { @@ -465,6 +604,25 @@ 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") @@ -496,3 +654,135 @@ func TestRBACModelInMultiLines(t *testing.T) { 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 0561c48ca..800e7570b 100644 --- a/persist/adapter.go +++ b/persist/adapter.go @@ -15,25 +15,49 @@ package persist import ( + "encoding/csv" "strings" - "github.com/casbin/casbin/v2/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) { +func LoadPolicyLine(line string, m model.Model) error { if line == "" || strings.HasPrefix(line, "#") { - return + return nil } - tokens := strings.Split(line, ",") - for i := 0; i < len(tokens); i++ { - tokens[i] = strings.TrimSpace(tokens[i]) + r := csv.NewReader(strings.NewReader(line)) + r.Comma = ',' + r.Comment = '#' + r.TrimLeadingSpace = true + + tokens, err := r.Read() + if err != nil { + return err } - key := tokens[0] + return LoadPolicyArray(tokens, m) +} + +// 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 82c9a0e7c..f4be4a6bd 100644 --- a/persist/adapter_filtered.go +++ b/persist/adapter_filtered.go @@ -15,7 +15,7 @@ package persist import ( - "github.com/casbin/casbin/v2/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 e0850bb7a..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/v2/model" - "github.com/casbin/casbin/v2/persist" - "github.com/casbin/casbin/v2/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,6 +32,18 @@ type Adapter struct { filePath string } +func (a *Adapter) UpdatePolicy(sec string, ptype string, oldRule, newRule []string) error { + return errors.New("not implemented") +} + +func (a *Adapter) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error { + return errors.New("not implemented") +} + +func (a *Adapter) UpdateFilteredPolicies(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) { + return nil, errors.New("not implemented") +} + // NewAdapter is the constructor for Adapter. func NewAdapter(filePath string) *Adapter { return &Adapter{filePath: filePath} @@ -73,7 +85,7 @@ func (a *Adapter) SavePolicy(model model.Model) error { 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 @@ -83,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() } @@ -113,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 924ef2ddb..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/v2/model" - "github.com/casbin/casbin/v2/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,8 +34,13 @@ 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. @@ -72,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 @@ -87,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() } @@ -119,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 213d99fc7..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/v2/model" - "github.com/casbin/casbin/v2/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,21 +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 +// 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 +// GetMockErr returns a mock error or nil. func (a *AdapterMock) GetMockErr() error { var returnError error if a.errorValue != "" { @@ -89,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_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 cf2c53048..d6e9d164e 100644 --- a/rbac/default-role-manager/role_manager.go +++ b/rbac/default-role-manager/role_manager.go @@ -15,284 +15,1236 @@ package defaultrolemanager import ( + "errors" "sync" - "github.com/casbin/casbin/v2/errors" - "github.com/casbin/casbin/v2/log" - "github.com/casbin/casbin/v2/rbac" + "github.com/casbin/casbin/v3/rbac" + "github.com/casbin/casbin/v3/util" ) -type MatchingFunc func(arg1, arg2 string) bool +const defaultDomain string = "" -// RoleManager provides a default implementation for the RoleManager interface -type RoleManager struct { - allRoles *sync.Map - maxHierarchyLevel int - hasPattern bool - matchingFunc MatchingFunc +// Role represents the data structure for a role in RBAC. +type Role struct { + name string + roles *sync.Map + users *sync.Map + matched *sync.Map + matchedBy *sync.Map + linkConditionFuncMap *sync.Map + linkConditionFuncParamsMap *sync.Map +} + +func newRole(name string) *Role { + r := Role{} + r.name = name + r.roles = &sync.Map{} + r.users = &sync.Map{} + r.matched = &sync.Map{} + r.matchedBy = &sync.Map{} + r.linkConditionFuncMap = &sync.Map{} + r.linkConditionFuncParamsMap = &sync.Map{} + return &r +} + +func (r *Role) addRole(role *Role) { + r.roles.Store(role.name, role) + role.addUser(r) +} + +func (r *Role) removeRole(role *Role) { + r.roles.Delete(role.name) + role.removeUser(r) +} + +// should only be called inside addRole. +func (r *Role) addUser(user *Role) { + r.users.Store(user.name, user) +} + +// should only be called inside removeRole. +func (r *Role) removeUser(user *Role) { + r.users.Delete(user.name) +} + +func (r *Role) addMatch(role *Role) { + r.matched.Store(role.name, role) + role.matchedBy.Store(r.name, r) +} + +func (r *Role) removeMatch(role *Role) { + r.matched.Delete(role.name) + role.matchedBy.Delete(r.name) +} + +func (r *Role) removeMatches() { + r.matched.Range(func(key, value interface{}) bool { + r.removeMatch(value.(*Role)) + return true + }) + r.matchedBy.Range(func(key, value interface{}) bool { + value.(*Role).removeMatch(r) + return true + }) +} + +func (r *Role) rangeRoles(fn func(key, value interface{}) bool) { + r.roles.Range(fn) + r.roles.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.matched.Range(fn) + return true + }) + r.matchedBy.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.roles.Range(fn) + return true + }) +} + +func (r *Role) rangeUsers(fn func(key, value interface{}) bool) { + r.users.Range(fn) + r.users.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.matched.Range(fn) + return true + }) + r.matchedBy.Range(func(key, value interface{}) bool { + role := value.(*Role) + role.users.Range(fn) + return true + }) +} + +func (r *Role) getRoles() []string { + var names []string + r.rangeRoles(func(key, value interface{}) bool { + names = append(names, key.(string)) + return true + }) + return util.RemoveDuplicateElement(names) +} + +func (r *Role) getUsers() []string { + var names []string + r.rangeUsers(func(key, value interface{}) bool { + names = append(names, key.(string)) + return true + }) + return names +} + +type linkConditionFuncKey struct { + roleName string + domainName string +} + +func (r *Role) addLinkConditionFunc(role *Role, domain string, fn rbac.LinkConditionFunc) { + r.linkConditionFuncMap.Store(linkConditionFuncKey{role.name, domain}, fn) +} + +func (r *Role) getLinkConditionFunc(role *Role, domain string) (rbac.LinkConditionFunc, bool) { + fn, ok := r.linkConditionFuncMap.Load(linkConditionFuncKey{role.name, domain}) + if fn == nil { + return nil, ok + } + return fn.(rbac.LinkConditionFunc), ok } -// NewRoleManager is the constructor for creating an instance of the +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 - rm.hasPattern = false - return &rm } -func (rm *RoleManager) AddMatchingFunc(name string, fn MatchingFunc) { - rm.hasPattern = true +// use this constructor to avoid rebuild of AddMatchingFunc. +func newRoleManagerWithMatchingFunc(maxHierarchyLevel int, fn rbac.MatchingFunc) *RoleManagerImpl { + rm := NewRoleManagerImpl(maxHierarchyLevel) rm.matchingFunc = fn + return rm } -func (rm *RoleManager) hasRole(name string) bool { - var ok bool - if rm.hasPattern { - rm.allRoles.Range(func(key, value interface{}) bool { - if rm.matchingFunc(name, key.(string)) { - ok = true - } - return true - }) +// 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 { - _, ok = rm.allRoles.Load(name) + return false } +} - return ok +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 *RoleManager) createRole(name string) *Role { - if rm.hasPattern { - rm.allRoles.Range(func(key, value interface{}) bool { - if rm.matchingFunc(name, key.(string)) { - name = key.(string) - } - return true - }) +func (rm *RoleManagerImpl) load(name interface{}) (value *Role, ok bool) { + if r, ok := rm.allRoles.Load(name); ok { + return r.(*Role), true + } + return nil, false +} + +// loads or creates a role. +func (rm *RoleManagerImpl) getRole(name string) (r *Role, created bool) { + var role *Role + var ok bool + + if role, ok = rm.load(name); !ok { + role = newRole(name) + rm.allRoles.Store(name, role) + + if rm.matchingFunc != nil { + rm.rangeMatchingRoles(name, false, func(r *Role) bool { + r.addMatch(role) + return true + }) + + rm.rangeMatchingRoles(name, true, func(r *Role) bool { + role.addMatch(r) + return true + }) + } + } + + return role, !ok +} + +func loadAndDelete(m *sync.Map, name string) (value interface{}, loaded bool) { + value, loaded = m.Load(name) + if loaded { + m.Delete(name) + } + return value, loaded +} + +func (rm *RoleManagerImpl) removeRole(name string) { + if role, ok := loadAndDelete(rm.allRoles, name); ok { + role.(*Role).removeMatches() } - role, _ := rm.allRoles.LoadOrStore(name, newRole(name)) - return role.(*Role) +} + +// 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.ERR_DOMAIN_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.ERR_DOMAIN_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 } - if !rm.hasRole(name1) || !rm.hasRole(name2) { - return errors.ERR_NAMES12_NOT_FOUND + // 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) } - role1 := rm.createRole(name1) - role2 := rm.createRole(name2) - role1.deleteRole(role2) - return nil + return rm.hasLinkHelper(role.name, map[string]*Role{user.name: user}, rm.maxHierarchyLevel), 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.ERR_DOMAIN_PARAMETER +func (rm *RoleManagerImpl) hasLinkHelper(targetName string, roles map[string]*Role, level int) bool { + if level < 0 || len(roles) == 0 { + return false } - if name1 == name2 { - return true, nil + 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 + }) } - if !rm.hasRole(name1) || !rm.hasRole(name2) { - return false, nil + 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 +} - role1 := rm.createRole(name1) - return role1.hasRole(name2, rm.maxHierarchyLevel), 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 } -// 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.ERR_DOMAIN_PARAMETER +// 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(name) { - return []string{}, nil + 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) } - roles := rm.createRole(name).getRoles() - if len(domain) == 1 { - for i := range roles { - roles[i] = roles[i][len(domain[0])+2:] - } + 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 } - return roles, nil + + nextRoles := map[string]*Role{} + for _, role := range roles { + role.rangeRoles(func(key, value interface{}) bool { + roleName := key.(string) + if _, ok := roleSet[roleName]; !ok { + res = append(res, roleName) + roleSet[roleName] = true + nextRoles[roleName] = value.(*Role) + } + return true + }) + } + + return rm.getImplicitRolesHelper(nextRoles, roleSet, res, level+1) } -// 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 len(domain) == 1 { - name = domain[0] + "::" + name - } else if len(domain) > 1 { - return nil, errors.ERR_DOMAIN_PARAMETER +// 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 } - if !rm.hasRole(name) { - return nil, errors.ERR_NAME_NOT_FOUND + 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 + }) } - names := []string{} - rm.allRoles.Range(func(_, value interface{}) bool { - role := value.(*Role) - if role.hasDirectRole(name) { - names = append(names, role.name) - } + return rm.getImplicitUsersHelper(nextUsers, userSet, res, level+1) +} + +// PrintRoles prints all the roles to log. +func (rm *RoleManagerImpl) PrintRoles() error { + // Logger has been removed - this is now a no-op + return nil +} + +// GetDomains gets domains that a user has. +func (rm *RoleManagerImpl) GetDomains(name string) ([]string, error) { + domains := []string{defaultDomain} + return domains, nil +} + +// GetAllDomains gets all domains. +func (rm *RoleManagerImpl) GetAllDomains() ([]string, error) { + domains := []string{defaultDomain} + return domains, nil +} + +func (rm *RoleManagerImpl) copyFrom(other *RoleManagerImpl) { + other.Range(func(name1, name2 string, domain ...string) bool { + _ = rm.AddLink(name1, name2, domain...) + return true + }) +} + +func rangeLinks(users *sync.Map, fn func(name1, name2 string, domain ...string) bool) { + users.Range(func(_, value interface{}) bool { + user := value.(*Role) + user.roles.Range(func(key, _ interface{}) bool { + roleName := key.(string) + return fn(user.name, roleName, defaultDomain) + }) + return true + }) +} + +func (rm *RoleManagerImpl) Range(fn func(name1, name2 string, domain ...string) bool) { + rangeLinks(rm.allRoles, fn) +} + +// Deprecated: BuildRelationship is no longer required. +func (rm *RoleManagerImpl) BuildRelationship(name1 string, name2 string, domain ...string) error { + return nil +} + +type DomainManager struct { + rmMap *sync.Map + maxHierarchyLevel int + matchingFunc rbac.MatchingFunc + domainMatchingFunc rbac.MatchingFunc + matchingFuncCache *util.SyncLRUCache +} + +// NewDomainManager is the constructor for creating an instance of the +// default DomainManager implementation. +func NewDomainManager(maxHierarchyLevel int) *DomainManager { + dm := &DomainManager{} + _ = dm.Clear() // init rmMap and rmCache + dm.maxHierarchyLevel = maxHierarchyLevel + return dm +} + +// AddMatchingFunc support use pattern in g. +func (dm *DomainManager) AddMatchingFunc(name string, fn rbac.MatchingFunc) { + dm.matchingFunc = fn + dm.rmMap.Range(func(key, value interface{}) bool { + value.(*RoleManagerImpl).AddMatchingFunc(name, fn) return true }) - if len(domain) == 1 { - for i := range names { - names[i] = names[i][len(domain[0])+2:] +} + +// AddDomainMatchingFunc support use domain pattern in g. +func (dm *DomainManager) AddDomainMatchingFunc(name string, fn rbac.MatchingFunc) { + dm.domainMatchingFunc = fn + dm.rmMap.Range(func(key, value interface{}) bool { + value.(*RoleManagerImpl).AddDomainMatchingFunc(name, fn) + return true + }) + dm.rebuild() +} + +// clears the map of RoleManagers. +func (dm *DomainManager) rebuild() { + rmMap := dm.rmMap + _ = dm.Clear() + rmMap.Range(func(key, value interface{}) bool { + domain := key.(string) + rm := value.(*RoleManagerImpl) + + rm.Range(func(name1, name2 string, _ ...string) bool { + _ = dm.AddLink(name1, name2, domain) + return true + }) + return true + }) +} + +// Clear clears all stored data and resets the role manager to the initial state. +func (dm *DomainManager) Clear() error { + dm.rmMap = &sync.Map{} + dm.matchingFuncCache = util.NewSyncLRUCache(100) + return nil +} + +func (dm *DomainManager) getDomain(domains ...string) (domain string, err error) { + switch len(domains) { + case 0: + return defaultDomain, nil + default: + return domains[0], nil + } +} + +func (dm *DomainManager) Match(str string, pattern string) bool { + if str == pattern { + return true + } + + if dm.domainMatchingFunc != nil { + return dm.domainMatchingFunc(str, pattern) + } else { + return false + } +} + +func (dm *DomainManager) rangeAffectedRoleManagers(domain string, fn func(rm *RoleManagerImpl)) { + if dm.domainMatchingFunc != nil { + dm.rmMap.Range(func(key, value interface{}) bool { + domain2 := key.(string) + if domain != domain2 && dm.Match(domain2, domain) { + fn(value.(*RoleManagerImpl)) + } + return true + }) + } +} + +func (dm *DomainManager) load(name interface{}) (value *RoleManagerImpl, ok bool) { + if r, ok := dm.rmMap.Load(name); ok { + return r.(*RoleManagerImpl), true + } + return nil, false +} + +// load or create a RoleManager instance of domain. +func (dm *DomainManager) getRoleManager(domain string, store bool) *RoleManagerImpl { + var rm *RoleManagerImpl + var ok bool + + if rm, ok = dm.load(domain); !ok { + rm = newRoleManagerWithMatchingFunc(dm.maxHierarchyLevel, dm.matchingFunc) + if store { + dm.rmMap.Store(domain, rm) } + if dm.domainMatchingFunc != nil { + dm.rmMap.Range(func(key, value interface{}) bool { + domain2 := key.(string) + rm2 := value.(*RoleManagerImpl) + if domain != domain2 && dm.Match(domain, domain2) { + rm.copyFrom(rm2) + } + return true + }) + } + } + return rm +} + +// AddLink adds the inheritance link between role: name1 and role: name2. +// aka role: name1 inherits role: name2. +func (dm *DomainManager) AddLink(name1 string, name2 string, domains ...string) error { + domain, err := dm.getDomain(domains...) + if err != nil { + return err + } + roleManager := dm.getRoleManager(domain, true) // create role manager if it does not exist + _ = roleManager.AddLink(name1, name2, domains...) + + dm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.AddLink(name1, name2, domains...) + }) + return nil +} + +// DeleteLink deletes the inheritance link between role: name1 and role: name2. +// aka role: name1 does not inherit role: name2 any more. +func (dm *DomainManager) DeleteLink(name1 string, name2 string, domains ...string) error { + domain, err := dm.getDomain(domains...) + if err != nil { + return err + } + roleManager := dm.getRoleManager(domain, true) // create role manager if it does not exist + _ = roleManager.DeleteLink(name1, name2, domains...) + + dm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.DeleteLink(name1, name2, domains...) + }) + return nil +} + +// HasLink determines whether role: name1 inherits role: name2. +func (dm *DomainManager) HasLink(name1 string, name2 string, domains ...string) (bool, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return false, err + } + rm := dm.getRoleManager(domain, false) + return rm.HasLink(name1, name2, domains...) +} + +// GetRoles gets the roles that a subject inherits. +func (dm *DomainManager) GetRoles(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err } - return names, nil + rm := dm.getRoleManager(domain, false) + return rm.GetRoles(name, domains...) +} + +// GetUsers gets the users of a role. +func (dm *DomainManager) GetUsers(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err + } + rm := dm.getRoleManager(domain, false) + return rm.GetUsers(name, domains...) +} + +// GetImplicitRoles gets the implicit roles that a subject inherits, respecting maxHierarchyLevel. +func (dm *DomainManager) GetImplicitRoles(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err + } + rm := dm.getRoleManager(domain, false) + return rm.GetImplicitRoles(name, domains...) +} + +// GetImplicitUsers gets the implicit users that inherits a role, respecting maxHierarchyLevel. +func (dm *DomainManager) GetImplicitUsers(name string, domains ...string) ([]string, error) { + domain, err := dm.getDomain(domains...) + if err != nil { + return nil, err + } + rm := dm.getRoleManager(domain, false) + return rm.GetImplicitUsers(name, domains...) } // PrintRoles prints all the roles to log. -func (rm *RoleManager) PrintRoles() error { - line := "" - rm.allRoles.Range(func(_, value interface{}) bool { - if text := value.(*Role).toString(); text != "" { - if line == "" { - line = text - } else { - line += ", " + text - } +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 }) - log.LogPrint(line) + return domains, nil +} + +// GetAllDomains gets all domains. +func (dm *DomainManager) GetAllDomains() ([]string, error) { + var domains []string + dm.rmMap.Range(func(key, value interface{}) bool { + domains = append(domains, key.(string)) + return true + }) + return domains, nil +} + +// Deprecated: BuildRelationship is no longer required. +func (dm *DomainManager) BuildRelationship(name1 string, name2 string, domain ...string) error { return nil } -// 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) + }) + } + + return crm.hasLinkHelper(targetName, nextRoles, level-1) +} + +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 } - r.roles = append(r.roles, role) + if passLinkConditionFunc { + nextRoles[nextRole.name] = nextRole + } + + return true } -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) 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 false + 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) } -func (r *Role) toString() string { - names := "" - if len(r.roles) == 0 { - return "" +// 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 } - for i, role := range r.roles { - if i == 0 { - names += role.name - } else { - names += ", " + role.name - } + + 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 len(r.roles) == 1 { - return r.name + " < " + names + if roleCreated { + crm.removeRole(role.name) + return nil, false + } + + domainName := defaultDomain + if len(domain) != 0 { + domainName = domain[0] + } + + if params, ok := user.getLinkConditionFuncParams(role, domainName); ok { + return params, true } else { - return r.name + " < (" + names + ")" + return nil, false } } -func (r *Role) getRoles() []string { - names := []string{} - for _, role := range r.roles { - names = append(names, role.name) +// AddLinkConditionFunc is based on userName, roleName, add LinkConditionFunc. +func (crm *ConditionalRoleManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) { + crm.AddDomainLinkConditionFunc(userName, roleName, defaultDomain, fn) +} + +// AddDomainLinkConditionFunc is based on userName, roleName, domain, add LinkConditionFunc. +func (crm *ConditionalRoleManager) AddDomainLinkConditionFunc(userName, roleName, domain string, fn rbac.LinkConditionFunc) { + user, _ := crm.getRole(userName) + role, _ := crm.getRole(roleName) + + user.addLinkConditionFunc(role, domain, fn) +} + +// SetLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain. +func (crm *ConditionalRoleManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) { + crm.SetDomainLinkConditionFuncParams(userName, roleName, defaultDomain, params...) +} + +// SetDomainLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain. +func (crm *ConditionalRoleManager) SetDomainLinkConditionFuncParams(userName, roleName, domain string, params ...string) { + user, _ := crm.getRole(userName) + role, _ := crm.getRole(roleName) + + user.setLinkConditionFuncParams(role, domain, params...) +} + +type ConditionalDomainManager struct { + ConditionalRoleManager + DomainManager +} + +// NewConditionalDomainManager is the constructor for creating an instance of the +// ConditionalDomainManager implementation. +func NewConditionalDomainManager(maxHierarchyLevel int) *ConditionalDomainManager { + rm := ConditionalDomainManager{} + _ = rm.Clear() // init allRoles and matchingFuncCache + rm.maxHierarchyLevel = maxHierarchyLevel + return &rm +} + +func (cdm *ConditionalDomainManager) load(name interface{}) (value *ConditionalRoleManager, ok bool) { + if r, ok := cdm.rmMap.Load(name); ok { + return r.(*ConditionalRoleManager), true } - return names + return nil, false +} + +// load or create a ConditionalRoleManager instance of domain. +func (cdm *ConditionalDomainManager) getConditionalRoleManager(domain string, store bool) *ConditionalRoleManager { + var rm *ConditionalRoleManager + var ok bool + + if rm, ok = cdm.load(domain); !ok { + rm = newConditionalRoleManagerWithMatchingFunc(cdm.maxHierarchyLevel, cdm.matchingFunc) + if store { + cdm.rmMap.Store(domain, rm) + } + if cdm.domainMatchingFunc != nil { + cdm.rmMap.Range(func(key, value interface{}) bool { + domain2 := key.(string) + rm2 := value.(*ConditionalRoleManager) + if domain != domain2 && cdm.Match(domain, domain2) { + rm.copyFrom(rm2) + } + return true + }) + } + } + return rm +} + +// HasLink determines whether role: name1 inherits role: name2. +func (cdm *ConditionalDomainManager) HasLink(name1 string, name2 string, domains ...string) (bool, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return false, err + } + rm := cdm.getConditionalRoleManager(domain, false) + return rm.HasLink(name1, name2, domains...) +} + +func (cdm *ConditionalDomainManager) GetRoles(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetRoles(name, domains...) +} + +func (cdm *ConditionalDomainManager) GetUsers(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetUsers(name, domains...) +} + +func (cdm *ConditionalDomainManager) GetImplicitRoles(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetImplicitRoles(name, domains...) +} + +func (cdm *ConditionalDomainManager) GetImplicitUsers(name string, domains ...string) ([]string, error) { + domain, err := cdm.getDomain(domains...) + if err != nil { + return nil, err + } + crm := cdm.getConditionalRoleManager(domain, false) + return crm.GetImplicitUsers(name, domains...) +} + +// AddLink adds the inheritance link between role: name1 and role: name2. +// aka role: name1 inherits role: name2. +func (cdm *ConditionalDomainManager) AddLink(name1 string, name2 string, domains ...string) error { + domain, err := cdm.getDomain(domains...) + if err != nil { + return err + } + conditionalRoleManager := cdm.getConditionalRoleManager(domain, true) // create role manager if it does not exist + _ = conditionalRoleManager.AddLink(name1, name2, domain) + + cdm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.AddLink(name1, name2, domain) + }) + return nil +} + +// DeleteLink deletes the inheritance link between role: name1 and role: name2. +// aka role: name1 does not inherit role: name2 any more. +func (cdm *ConditionalDomainManager) DeleteLink(name1 string, name2 string, domains ...string) error { + domain, err := cdm.getDomain(domains...) + if err != nil { + return err + } + conditionalRoleManager := cdm.getConditionalRoleManager(domain, true) // create role manager if it does not exist + _ = conditionalRoleManager.DeleteLink(name1, name2, domain) + + cdm.rangeAffectedRoleManagers(domain, func(rm *RoleManagerImpl) { + _ = rm.DeleteLink(name1, name2, domain) + }) + return nil +} + +// AddLinkConditionFunc is based on userName, roleName, add LinkConditionFunc. +func (cdm *ConditionalDomainManager) AddLinkConditionFunc(userName, roleName string, fn rbac.LinkConditionFunc) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).AddLinkConditionFunc(userName, roleName, fn) + return true + }) +} + +// AddDomainLinkConditionFunc is based on userName, roleName, domain, add LinkConditionFunc. +func (cdm *ConditionalDomainManager) AddDomainLinkConditionFunc(userName, roleName, domain string, fn rbac.LinkConditionFunc) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).AddDomainLinkConditionFunc(userName, roleName, domain, fn) + return true + }) +} + +// SetLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName. +func (cdm *ConditionalDomainManager) SetLinkConditionFuncParams(userName, roleName string, params ...string) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).SetLinkConditionFuncParams(userName, roleName, params...) + return true + }) +} + +// SetDomainLinkConditionFuncParams sets parameters of LinkConditionFunc based on userName, roleName, domain. +func (cdm *ConditionalDomainManager) SetDomainLinkConditionFuncParams(userName, roleName, domain string, params ...string) { + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).SetDomainLinkConditionFuncParams(userName, roleName, domain, params...) + return true + }) +} + +// AddDomainMatchingFunc support use domain pattern in g. +func (cdm *ConditionalDomainManager) AddDomainMatchingFunc(name string, fn rbac.MatchingFunc) { + cdm.domainMatchingFunc = fn + cdm.rmMap.Range(func(key, value interface{}) bool { + value.(*ConditionalRoleManager).AddDomainMatchingFunc(name, fn) + return true + }) + cdm.rebuild() +} + +// rebuild clears the map of ConditionalRoleManagers. +func (cdm *ConditionalDomainManager) rebuild() { + rmMap := cdm.rmMap + _ = cdm.Clear() + rmMap.Range(func(key, value interface{}) bool { + domain := key.(string) + crm := value.(*ConditionalRoleManager) + + crm.Range(func(name1, name2 string, _ ...string) bool { + _ = cdm.AddLink(name1, name2, domain) + return true + }) + return true + }) } diff --git a/rbac/default-role-manager/role_manager_test.go b/rbac/default-role-manager/role_manager_test.go index 1e79af1db..679d5458b 100644 --- a/rbac/default-role-manager/role_manager_test.go +++ b/rbac/default-role-manager/role_manager_test.go @@ -15,10 +15,13 @@ package defaultrolemanager import ( + "fmt" + "sync" + "sync/atomic" "testing" - "github.com/casbin/casbin/v2/rbac" - "github.com/casbin/casbin/v2/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) { @@ -46,19 +49,38 @@ func testPrintRoles(t *testing.T, rm rbac.RoleManager, name string, res []string myRes, _ := rm.GetRoles(name) 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 @@ -88,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 @@ -118,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 @@ -156,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 @@ -189,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 @@ -203,7 +245,7 @@ func TestClear(t *testing.T) { // / \ // u1 u2 - rm.Clear() + _ = rm.Clear() // All data is cleared. // No role inheritance now. @@ -221,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 62071a87d..c1ca4f7a3 100644 --- a/rbac_api.go +++ b/rbac_api.go @@ -15,26 +15,39 @@ package casbin import ( - "errors" + "fmt" + "strings" - "github.com/casbin/casbin/v2/util" + "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, error) { - res, err := e.model["g"]["g"].RM.GetRoles(name) +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, error) { - res, err := e.model["g"]["g"].RM.GetUsers(name) +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, error) { - roles, err := 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 } @@ -51,38 +64,83 @@ func (e *Enforcer) HasRoleForUser(name string, role string) (bool, error) { // 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, error) { - 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, error) { - 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, error) { - 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, error) { - return e.RemoveFilteredGroupingPolicy(0, user) + var err error + res1, err := e.RemoveFilteredGroupingPolicy(0, user) + if err != nil { + return res1, err + } + + subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex) + if err != nil { + return false, err + } + res2, err := e.RemoveFilteredPolicy(subIndex, user) + return res1 || res2, err } // DeleteRole deletes a role. +// Returns false if the role does not exist (aka not affected). func (e *Enforcer) DeleteRole(role string) (bool, error) { var err error - res1, err := e.RemoveFilteredGroupingPolicy(1, role) + res1, err := e.RemoveFilteredGroupingPolicy(0, role) if err != nil { return res1, err } - res2, err := e.RemoveFilteredPolicy(0, role) - return res1 || res2, 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. @@ -97,6 +155,16 @@ func (e *Enforcer) AddPermissionForUser(user string, permission ...string) (bool return e.AddPolicy(util.JoinSlice(user, permission...)) } +// AddPermissionsForUser adds multiple permissions for a user or role. +// Returns false if the user or role already has one of the permissions (aka not affected). +func (e *Enforcer) AddPermissionsForUser(user string, permissions ...[]string) (bool, error) { + var rules [][]string + for _, permission := range permissions { + rules = append(rules, util.JoinSlice(user, permission...)) + } + return e.AddPolicies(rules) +} + // DeletePermissionForUser deletes a permission for a user or role. // Returns false if the user or role does not have the permission (aka not affected). func (e *Enforcer) DeletePermissionForUser(user string, permission ...string) (bool, error) { @@ -106,16 +174,51 @@ func (e *Enforcer) DeletePermissionForUser(user string, permission ...string) (b // DeletePermissionsForUser deletes permissions for a user or role. // Returns false if the user or role does not have any permissions (aka not affected). func (e *Enforcer) DeletePermissionsForUser(user string) (bool, error) { - return e.RemoveFilteredPolicy(0, user) + 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 { +func (e *Enforcer) HasPermissionForUser(user string, permission ...string) (bool, error) { return e.HasPolicy(util.JoinSlice(user, permission...)) } @@ -128,28 +231,65 @@ func (e *Enforcer) HasPermissionForUser(user string, permission ...string) bool // 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) { - res := []string{} - roleSet := make(map[string]bool) - roleSet[name] = true - - q := make([]string, 0) - q = append(q, name) + var res []string - for len(q) > 0 { - name := q[0] - q = q[1:] + for rm := range e.rmMap { + roles, err := e.GetNamedImplicitRolesForUser(rm, name, domain...) + if err != nil { + return nil, err + } + res = append(res, roles...) + } - roles, err := e.rm.GetRoles(name, domain...) + for crm := range e.condRmMap { + roles, err := e.GetNamedImplicitRolesForUser(crm, name, domain...) if err != nil { return nil, err } - for _, r := range roles { - if _, ok := roleSet[r]; !ok { - res = append(res, r) - q = append(q, r) - roleSet[r] = true - } + 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 @@ -165,32 +305,61 @@ func (e *Enforcer) GetImplicitRolesForUser(name string, domain ...string) ([]str // 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) { - roles, err := e.GetImplicitRolesForUser(user, domain...) + 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 } - - roles = append([]string{user}, roles...) - - withDomain := false - if len(domain) == 1 { - withDomain = true - } else if len(domain) > 1 { - return nil, errors.New("domain should be 1 parameter") + policyRoles := make(map[string]struct{}, len(roles)+1) + policyRoles[user] = struct{}{} + for _, r := range roles { + policyRoles[r] = struct{}{} } - res := [][]string{} - permissions := [][]string{} - for _, role := range roles { - if withDomain { - permissions = e.GetPermissionsForUserInDomain(role, domain[0]) - } else { - permissions = e.GetPermissionsForUser(role) + 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) } - res = append(res, permissions...) } - - return res, nil + return permission, nil } // GetImplicitUsersForPermission gets implicit users for a permission. @@ -202,13 +371,26 @@ func (e *Enforcer) GetImplicitPermissionsForUser(user string, domain ...string) // 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) { - subjects := e.GetAllSubjects() - roles := e.GetAllRoles() + 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) - users := util.SetSubtract(subjects, roles) + subjects = util.SetSubtract(subjects, gInherit) res := []string{} - for _, user := range users { + for _, user := range subjects { req := util.JoinSliceAny(user, permission...) allowed, err := e.Enforce(req...) if err != nil { @@ -222,3 +404,327 @@ func (e *Enforcer) GetImplicitUsersForPermission(permission ...string) ([]string 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 3f060ad0f..8767bb671 100644 --- a/rbac_api_synced.go +++ b/rbac_api_synced.go @@ -15,48 +15,56 @@ package casbin // GetRolesForUser gets the roles that a user has. -func (e *SyncedEnforcer) GetRolesForUser(name string) ([]string, error) { +func (e *SyncedEnforcer) GetRolesForUser(name string, domain ...string) ([]string, error) { e.m.RLock() defer e.m.RUnlock() - return e.Enforcer.GetRolesForUser(name) + return e.Enforcer.GetRolesForUser(name, domain...) } // GetUsersForRole gets the users that has a role. -func (e *SyncedEnforcer) GetUsersForRole(name string) ([]string, error) { +func (e *SyncedEnforcer) GetUsersForRole(name string, domain ...string) ([]string, error) { e.m.RLock() defer e.m.RUnlock() - return e.Enforcer.GetUsersForRole(name) + return e.Enforcer.GetUsersForRole(name, domain...) } // HasRoleForUser determines whether a user has a role. -func (e *SyncedEnforcer) HasRoleForUser(name string, role string) (bool, error) { +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) + 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, error) { +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) + return e.Enforcer.AddRoleForUser(user, role, domain...) +} + +// AddRolesForUser adds roles for a user. +// Returns false if the user already has the roles (aka not affected). +func (e *SyncedEnforcer) AddRolesForUser(user string, roles []string, domain ...string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddRolesForUser(user, roles, domain...) } // DeleteRoleForUser deletes a role for a user. // Returns false if the user does not have the role (aka not affected). -func (e *SyncedEnforcer) DeleteRoleForUser(user string, role string) (bool, error) { +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, error) { +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. @@ -68,10 +76,11 @@ func (e *SyncedEnforcer) DeleteUser(user string) (bool, error) { } // 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. @@ -90,6 +99,14 @@ func (e *SyncedEnforcer) AddPermissionForUser(user string, permission ...string) return e.Enforcer.AddPermissionForUser(user, permission...) } +// AddPermissionsForUser adds permissions for a user or role. +// Returns false if the user or role already has the permissions (aka not affected). +func (e *SyncedEnforcer) AddPermissionsForUser(user string, permissions ...[]string) (bool, error) { + e.m.Lock() + defer e.m.Unlock() + return e.Enforcer.AddPermissionsForUser(user, permissions...) +} + // DeletePermissionForUser deletes a permission for a user or role. // Returns false if the user or role does not have the permission (aka not affected). func (e *SyncedEnforcer) DeletePermissionForUser(user string, permission ...string) (bool, error) { @@ -107,15 +124,95 @@ func (e *SyncedEnforcer) DeletePermissionsForUser(user string) (bool, error) { } // GetPermissionsForUser gets permissions for a user or role. -func (e *SyncedEnforcer) GetPermissionsForUser(user string) [][]string { +func (e *SyncedEnforcer) GetPermissionsForUser(user string, domain ...string) ([][]string, error) { e.m.RLock() defer e.m.RUnlock() - return e.Enforcer.GetPermissionsForUser(user) + 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.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 732a439b7..6d88eded1 100644 --- a/rbac_api_test.go +++ b/rbac_api_test.go @@ -15,15 +15,20 @@ package casbin import ( + "fmt" + "log" + "sort" "testing" - "github.com/casbin/casbin/v2/errors" - "github.com/casbin/casbin/v2/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, err := e.GetRolesForUser(name) + myRes, err := e.GetRolesForUser(name, domain...) if err != nil { t.Error("Roles for ", name, " could not be fetched: ", err.Error()) } @@ -34,13 +39,13 @@ func testGetRoles(t *testing.T, e *Enforcer, name string, res []string) { } } -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, err := e.GetUsersForRole(name) + myRes, err := e.GetUsersForRole(name, domain...) switch err { case nil: break - case errors.ERR_NAME_NOT_FOUND: + case errors.ErrNameNotFound: t.Log("No name found") default: t.Error("Users for ", name, " could not be fetched: ", err.Error()) @@ -52,9 +57,9 @@ func testGetUsers(t *testing.T, e *Enforcer, name string, res []string) { } } -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, err := e.HasRoleForUser(name, role) + myRes, err := e.HasRoleForUser(name, role, domain...) if err != nil { t.Error("HasRoleForUser returned an error: ", err.Error()) } @@ -68,42 +73,42 @@ 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") - 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) @@ -112,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) @@ -124,9 +129,74 @@ 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) + myRes, err := e.GetPermissionsForUser(name, domain...) + if err != nil { + t.Error(err.Error()) + } t.Log("Permissions for ", name, ": ", myRes) if !util.Array2DEquals(res, myRes) { @@ -136,7 +206,11 @@ 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...) + 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 { @@ -144,6 +218,19 @@ func testHasPermission(t *testing.T, e *Enforcer, name string, permission []stri } } +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") @@ -160,33 +247,44 @@ 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) { @@ -194,7 +292,7 @@ func testGetImplicitRoles(t *testing.T, e *Enforcer, name string, res []string) myRes, _ := e.GetImplicitRolesForUser(name) t.Log("Implicit roles for ", name, ": ", myRes) - if !util.ArrayEquals(res, myRes) { + if !util.SetEquals(res, myRes) { t.Error("Implicit roles for ", name, ": ", myRes, ", supposed to be ", res) } } @@ -204,7 +302,7 @@ func testGetImplicitRolesInDomain(t *testing.T, e *Enforcer, name string, domain myRes, _ := e.GetImplicitRolesForUser(name, domain) t.Log("Implicit roles in domain ", domain, " for ", name, ": ", myRes) - if !util.ArrayEquals(res, myRes) { + if !util.SetEquals(res, myRes) { t.Error("Implicit roles in domain ", domain, " for ", name, ": ", myRes, ", supposed to be ", res) } } @@ -217,14 +315,23 @@ func TestImplicitRoleAPI(t *testing.T) { 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) { +func testGetImplicitPermissions(t *testing.T, e *Enforcer, name string, res [][]string, domain ...string) { t.Helper() - myRes, _ := e.GetImplicitPermissionsForUser(name) + myRes, _ := e.GetImplicitPermissionsForUser(name, domain...) t.Log("Implicit permissions for ", name, ": ", myRes) - if !util.Array2DEquals(res, myRes) { + if !util.Set2DEquals(res, myRes) { t.Error("Implicit permissions for ", name, ": ", myRes, ", supposed to be ", res) } } @@ -234,11 +341,21 @@ func testGetImplicitPermissionsWithDomain(t *testing.T, e *Enforcer, name string myRes, _ := e.GetImplicitPermissionsForUser(name, domain) t.Log("Implicit permissions for", name, "under", domain, ":", myRes) - if !util.Array2DEquals(res, 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") @@ -247,6 +364,28 @@ func TestImplicitPermissionAPI(t *testing.T) { 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) { @@ -259,6 +398,9 @@ func testGetImplicitUsers(t *testing.T, e *Enforcer, res []string, permission .. 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) } @@ -271,4 +413,471 @@ func TestImplicitUserAPI(t *testing.T) { 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 f1273cb17..2f4bc0f76 100644 --- a/rbac_api_with_domains.go +++ b/rbac_api_with_domains.go @@ -14,21 +14,34 @@ package casbin -// GetUsersForRoleInDomain gets the users that has a role inside a domain. Add by Gordon +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 { - res, _ := e.model["g"]["g"].RM.GetUsers(name, domain) + 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. @@ -42,3 +55,147 @@ func (e *Enforcer) AddRoleForUserInDomain(user string, role string, domain strin 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 index 8afe02715..02fadb148 100644 --- a/rbac_api_with_domains_synced.go +++ b/rbac_api_with_domains_synced.go @@ -14,24 +14,24 @@ package casbin -// GetUsersForRoleInDomain gets the users that has a role inside a domain. Add by Gordon +// 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.Lock() - defer e.m.Unlock() + 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.Lock() - defer e.m.Unlock() + 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.Lock() - defer e.m.Unlock() + e.m.RLock() + defer e.m.RUnlock() return e.Enforcer.GetPermissionsForUserInDomain(user, domain) } @@ -50,3 +50,19 @@ func (e *SyncedEnforcer) DeleteRoleForUserInDomain(user string, role string, dom 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 ad9642d15..956cfc4b6 100644 --- a/rbac_api_with_domains_test.go +++ b/rbac_api_with_domains_test.go @@ -15,12 +15,13 @@ package casbin import ( + "sort" "testing" - "github.com/casbin/casbin/v2/util" + "github.com/casbin/casbin/v3/util" ) -// testGetUsersInDomain: Add by Gordon +// testGetUsersInDomain: Add by Gordon. func testGetUsersInDomain(t *testing.T, e *Enforcer, name string, domain string, res []string) { t.Helper() myRes := e.GetUsersForRoleInDomain(name, domain) @@ -51,50 +52,117 @@ func TestGetImplicitRolesForDomainUser(t *testing.T) { testGetImplicitRolesInDomain(t, e, "alice", "domain1", []string{"role:global_admin", "role:reader", "role:writer"}) } -// TestUserAPIWithDomains: Add by Gordon +// 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") + _, _ = e.DeleteRoleForUserInDomain("alice", "admin", "domain1") + _, _ = e.AddRoleForUserInDomain("bob", "admin", "domain1") + testGetUsers(t, e, []string{"bob"}, "admin", "domain1") testGetUsersInDomain(t, e, "admin", "domain1", []string{"bob"}) + + testGetUsers(t, e, []string{}, "non_exist", "domain1") testGetUsersInDomain(t, e, "non_exist", "domain1", []string{}) + testGetUsers(t, e, []string{"bob"}, "admin", "domain2") testGetUsersInDomain(t, e, "admin", "domain2", []string{"bob"}) + + testGetUsers(t, e, []string{}, "non_exist", "domain2") testGetUsersInDomain(t, e, "non_exist", "domain2", []string{}) } func TestRoleAPIWithDomains(t *testing.T) { e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") + testGetRoles(t, e, []string{"admin"}, "alice", "domain1") testGetRolesInDomain(t, e, "alice", "domain1", []string{"admin"}) + + testGetRoles(t, e, []string{}, "bob", "domain1") testGetRolesInDomain(t, e, "bob", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "admin", "domain1") testGetRolesInDomain(t, e, "admin", "domain1", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain1") testGetRolesInDomain(t, e, "non_exist", "domain1", []string{}) + testGetRoles(t, e, []string{}, "alice", "domain2") testGetRolesInDomain(t, e, "alice", "domain2", []string{}) + + testGetRoles(t, e, []string{"admin"}, "bob", "domain2") testGetRolesInDomain(t, e, "bob", "domain2", []string{"admin"}) + + testGetRoles(t, e, []string{}, "admin", "domain2") testGetRolesInDomain(t, e, "admin", "domain2", []string{}) + + testGetRoles(t, e, []string{}, "non_exist", "domain2") testGetRolesInDomain(t, e, "non_exist", "domain2", []string{}) - e.DeleteRoleForUserInDomain("alice", "admin", "domain1") - e.AddRoleForUserInDomain("bob", "admin", "domain1") + _, _ = 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{}) } @@ -111,13 +179,247 @@ func testGetPermissionsInDomain(t *testing.T, e *Enforcer, name string, domain s func TestPermissionAPIInDomain(t *testing.T) { e, _ := NewEnforcer("examples/rbac_with_domains_model.conf", "examples/rbac_with_domains_policy.csv") - testGetPermissionsInDomain(t, e, "alice", "domain1", [][]string{}) + 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/util/builtin_operators.go b/util/builtin_operators.go index f86da4309..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/v2/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 bool(KeyMatch(name1, name2)), nil + return KeyMatch(name1, name2), nil +} + +// KeyGet returns the matched part +// For example, "/foo/bar/foo" matches "/foo/*" +// "bar/foo" will been returned. +func KeyGet(key1, key2 string) string { + i := strings.Index(key2, "*") + if i == -1 { + return "" + } + if len(key1) > i { + if key1[:i] == key2[:i] { + return key1[i:] + } + } + return "" +} + +// KeyGetFunc is the wrapper for KeyGet. +func KeyGetFunc(args ...interface{}) (interface{}, error) { + if err := validateVariadicArgs(2, args...); err != nil { + return false, fmt.Errorf("%s: %w", "keyGet", err) + } + + name1 := args[0].(string) + name2 := args[1].(string) + + return KeyGet(name1, name2), nil } // KeyMatch2 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. -// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource" +// 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+"$") } // 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) } + 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 bool(KeyMatch3(name1, name2)), nil + 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 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 a47897185..ba0f463db 100644 --- a/util/builtin_operators_test.go +++ b/util/builtin_operators_test.go @@ -40,6 +40,28 @@ 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) @@ -50,6 +72,19 @@ func testKeyMatch2(t *testing.T, key1 string, key2 string, res bool) { } } +func testGlobMatch(t *testing.T, key1 string, key2 string, res bool) { + t.Helper() + myRes, err := GlobMatch(key1, key2) + if err != nil { + panic(err) + } + t.Logf("%s < %s: %t", key1, key2, myRes) + + if myRes != res { + t.Errorf("%s < %s: %t, supposed to be %t", key1, key2, !res, res) + } +} + func TestKeyMatch2(t *testing.T) { testKeyMatch2(t, "/foo", "/foo", true) testKeyMatch2(t, "/foo", "/foo*", true) @@ -77,6 +112,51 @@ 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) { @@ -112,6 +192,84 @@ 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) { @@ -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 e25dc2231..b72823a4c 100644 --- a/util/util.go +++ b/util/util.go @@ -15,20 +15,35 @@ package util import ( + "encoding/json" "regexp" "sort" "strings" + "sync" ) +var evalReg = regexp.MustCompile(`\beval\((?P[^)]*)\)`) + +var escapeAssertionRegex = regexp.MustCompile(`([()\s|&,=!><+\-*/]|^)((r|p)[0-9]*)\.`) + +func JsonToMap(jsonStr string) (map[string]interface{}, error) { + result := make(map[string]interface{}) + err := json.Unmarshal([]byte(jsonStr), &result) + if err != nil { + return result, err + } + return result, nil +} + // EscapeAssertion escapes the dots in the assertion, because the expression evaluation doesn't support such variable names. func EscapeAssertion(s string) string { - //Replace the first dot, because it can't be recognized by the regexp. - if (strings.HasPrefix(s, "r") || strings.HasPrefix(s, "p")) { - s = strings.Replace(s, ".", "_",1) - } - var regex = regexp.MustCompile(`(\|| |=|\)|\(|&|<|>|,|\+|-|!|\*|\/)(r|p)\.`) - s = regex.ReplaceAllStringFunc(s, func(m string) string { - return strings.Replace(m, ".", "_", 1) + s = escapeAssertionRegex.ReplaceAllStringFunc(s, func(m string) string { + // Replace only the last dot with underscore (preserve the prefix character) + lastDotIdx := strings.LastIndex(m, ".") + if lastDotIdx > 0 { + return m[:lastDotIdx] + "_" + } + return m }) return s } @@ -70,6 +85,46 @@ func Array2DEquals(a [][]string, b [][]string) bool { return true } +// SortArray2D Sorts the two-dimensional string array. +func SortArray2D(arr [][]string) { + if len(arr) == 0 { + return + } + sort.Slice(arr, func(i, j int) bool { + minArrLen := len(arr[i]) + if len(arr[j]) < minArrLen { + minArrLen = len(arr[j]) + } + for k := 0; k < minArrLen; k++ { + if arr[i][k] != arr[j][k] { + return arr[i][k] < arr[j][k] + } + } + return len(arr[i]) < len(arr[j]) + }) +} + +// SortedArray2DEquals determines whether two 2-dimensional string arrays are identical. +func SortedArray2DEquals(a [][]string, b [][]string) bool { + if len(a) != len(b) { + return false + } + copyA := make([][]string, len(a)) + copy(copyA, a) + copyB := make([][]string, len(b)) + copy(copyB, b) + + SortArray2D(copyA) + SortArray2D(copyB) + + for i, v := range copyA { + if !ArrayEquals(v, copyB[i]) { + return false + } + } + return true +} + // ArrayRemoveDuplicates removes any duplicated elements in a string array. func ArrayRemoveDuplicates(s *[]string) { found := make(map[string]bool) @@ -111,14 +166,49 @@ func SetEquals(a []string, b []string) bool { return true } +// SetEquals determines whether two int sets are identical. +func SetEqualsInt(a []int, b []int) bool { + if len(a) != len(b) { + return false + } + + sort.Ints(a) + sort.Ints(b) + + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +// Set2DEquals determines whether two string slice sets are identical. +func Set2DEquals(a [][]string, b [][]string) bool { + if len(a) != len(b) { + return false + } + + var aa []string + for _, v := range a { + sort.Strings(v) + aa = append(aa, strings.Join(v, ", ")) + } + var bb []string + for _, v := range b { + sort.Strings(v) + bb = append(bb, strings.Join(v, ", ")) + } + + return SetEquals(aa, bb) +} + // JoinSlice joins a string and a slice into a new slice. func JoinSlice(a string, b ...string) []string { res := make([]string, 0, len(b)+1) res = append(res, a) - for _, s := range b { - res = append(res, s) - } + res = append(res, b...) return res } @@ -149,3 +239,187 @@ func SetSubtract(a []string, b []string) []string { } return diff } + +// HasEval determine whether matcher contains function eval. +func HasEval(s string) bool { + return evalReg.MatchString(s) +} + +// ReplaceEval replace function eval with the value of its parameters. +func ReplaceEval(s string, rule string) string { + return evalReg.ReplaceAllString(s, "("+rule+")") +} + +// ReplaceEvalWithMap replace function eval with the value of its parameters via given sets. +func ReplaceEvalWithMap(src string, sets map[string]string) string { + return evalReg.ReplaceAllStringFunc(src, func(s string) string { + subs := evalReg.FindStringSubmatch(s) + if subs == nil { + return s + } + key := subs[1] + value, found := sets[key] + if !found { + return s + } + return evalReg.ReplaceAllString(s, value) + }) +} + +// GetEvalValue returns the parameters of function eval. +func GetEvalValue(s string) []string { + subMatch := evalReg.FindAllStringSubmatch(s, -1) + var rules []string + for _, rule := range subMatch { + rules = append(rules, rule[1]) + } + return rules +} + +// EscapeStringLiterals escapes backslashes in string literals within an expression +// to ensure consistent handling between govaluate (which interprets escape sequences) +// and CSV parsing (which treats backslashes as literal characters). +// This function doubles all backslashes within single-quoted and double-quoted strings. +func EscapeStringLiterals(expr string) string { + var result strings.Builder + inString := false + var quote rune + + for i := 0; i < len(expr); i++ { + ch := rune(expr[i]) + + if inString { + result.WriteRune(ch) + if ch == '\\' { + // Found a backslash inside a string - double it + result.WriteRune('\\') + } else if ch == quote { + // End of string literal + inString = false + } + continue + } + + // Not inside a string literal + if ch == '\'' || ch == '"' { + inString = true + quote = ch + } + result.WriteRune(ch) + } + + return result.String() +} + +func RemoveDuplicateElement(s []string) []string { + result := make([]string, 0, len(s)) + temp := map[string]struct{}{} + for _, item := range s { + if _, ok := temp[item]; !ok { + temp[item] = struct{}{} + result = append(result, item) + } + } + return result +} + +type node struct { + key interface{} + value interface{} + prev *node + next *node +} + +type LRUCache struct { + capacity int + m map[interface{}]*node + head *node + tail *node +} + +func NewLRUCache(capacity int) *LRUCache { + cache := &LRUCache{} + cache.capacity = capacity + cache.m = map[interface{}]*node{} + + head := &node{} + tail := &node{} + + head.next = tail + tail.prev = head + + cache.head = head + cache.tail = tail + + return cache +} + +func (cache *LRUCache) remove(n *node, listOnly bool) { + if !listOnly { + delete(cache.m, n.key) + } + n.prev.next = n.next + n.next.prev = n.prev +} + +func (cache *LRUCache) add(n *node, listOnly bool) { + if !listOnly { + cache.m[n.key] = n + } + headNext := cache.head.next + cache.head.next = n + headNext.prev = n + n.next = headNext + n.prev = cache.head +} + +func (cache *LRUCache) moveToHead(n *node) { + cache.remove(n, true) + cache.add(n, true) +} + +func (cache *LRUCache) Get(key interface{}) (value interface{}, ok bool) { + n, ok := cache.m[key] + if ok { + cache.moveToHead(n) + return n.value, ok + } else { + return nil, ok + } +} + +func (cache *LRUCache) Put(key interface{}, value interface{}) { + n, ok := cache.m[key] + if ok { + cache.remove(n, false) + } else { + n = &node{key, value, nil, nil} + if len(cache.m) >= cache.capacity { + cache.remove(cache.tail.prev, false) + } + } + cache.add(n, false) +} + +type SyncLRUCache struct { + rwm sync.RWMutex + *LRUCache +} + +func NewSyncLRUCache(capacity int) *SyncLRUCache { + cache := &SyncLRUCache{} + cache.LRUCache = NewLRUCache(capacity) + return cache +} + +func (cache *SyncLRUCache) Get(key interface{}) (value interface{}, ok bool) { + cache.rwm.Lock() + defer cache.rwm.Unlock() + return cache.LRUCache.Get(key) +} + +func (cache *SyncLRUCache) Put(key interface{}, value interface{}) { + cache.rwm.Lock() + defer cache.rwm.Unlock() + cache.LRUCache.Put(key, value) +} diff --git a/util/util_test.go b/util/util_test.go index 9986f11f7..4697fde32 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -29,8 +29,13 @@ func testEscapeAssertion(t *testing.T, s string, res string) { } func TestEscapeAssertion(t *testing.T) { + testEscapeAssertion(t, "r_sub == r_obj.value", "r_sub == r_obj.value") + testEscapeAssertion(t, "p_sub == r_sub.value", "p_sub == r_sub.value") + testEscapeAssertion(t, "r.attr.value == p.attr", "r_attr.value == p_attr") testEscapeAssertion(t, "r.attr.value == p.attr", "r_attr.value == p_attr") testEscapeAssertion(t, "r.attp.value || p.attr", "r_attp.value || p_attr") + testEscapeAssertion(t, "r2.attr.value == p2.attr", "r2_attr.value == p2_attr") + testEscapeAssertion(t, "r2.attp.value || p2.attr", "r2_attp.value || p2_attr") testEscapeAssertion(t, "r.attp.value &&p.attr", "r_attp.value &&p_attr") testEscapeAssertion(t, "r.attp.value >p.attr", "r_attp.value >p_attr") testEscapeAssertion(t, "r.attp.value 0 { + if s, isString := rvals[0].(string); isString { + entry.Subject = s + } + } + if len(rvals) > 1 { + if o, isString := rvals[1].(string); isString { + entry.Object = o + } + } + if len(rvals) > 2 { + if a, isString := rvals[2].(string); isString { + entry.Action = a + } + } + if len(rvals) > 3 { + if d, isString := rvals[3].(string); isString { + entry.Domain = d + } + } + return entry +} + +// onLogBeforeEventInEnforce initializes logging for Enforce operation. +func (e *Enforcer) onLogBeforeEventInEnforce(rvals []interface{}) *log.LogEntry { + if e.logger == nil { + return nil + } + logEntry := e.createEnforceLogEntry(rvals) + _ = e.logger.OnBeforeEvent(logEntry) + return logEntry +} + +// onLogAfterEventInEnforce finalizes logging for Enforce operation. +func (e *Enforcer) onLogAfterEventInEnforce(logEntry *log.LogEntry, allowed bool) { + if e.logger != nil && logEntry != nil { + logEntry.Allowed = allowed + _ = e.logger.OnAfterEvent(logEntry) + } +} + +// logPolicyOperation logs a policy operation (add or remove) with before and after events. +func (e *Enforcer) logPolicyOperation(eventType log.EventType, sec string, rule []string, operation func() (bool, error)) (bool, error) { + var logEntry *log.LogEntry + if e.logger != nil && sec == "p" { + logEntry = &log.LogEntry{ + EventType: eventType, + Rules: [][]string{rule}, + } + _ = e.logger.OnBeforeEvent(logEntry) + } + + ok, err := operation() + + if e.logger != nil && logEntry != nil { + if ok && err == nil { + logEntry.RuleCount = 1 + } else { + logEntry.RuleCount = 0 + logEntry.Error = err + } + _ = e.logger.OnAfterEvent(logEntry) + } + + return ok, err +} diff --git a/watcher_ex_test.go b/watcher_ex_test.go new file mode 100644 index 000000000..ddde31392 --- /dev/null +++ b/watcher_ex_test.go @@ -0,0 +1,70 @@ +// 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 ( + "testing" + + "github.com/casbin/casbin/v3/model" +) + +type SampleWatcherEx struct { + SampleWatcher +} + +func (w SampleWatcherEx) UpdateForAddPolicy(sec, ptype string, params ...string) error { + return nil +} +func (w SampleWatcherEx) UpdateForRemovePolicy(sec, ptype string, params ...string) error { + return nil +} + +func (w SampleWatcherEx) UpdateForRemoveFilteredPolicy(sec, ptype string, fieldIndex int, fieldValues ...string) error { + return nil +} + +func (w SampleWatcherEx) UpdateForSavePolicy(model model.Model) error { + return nil +} + +func (w SampleWatcherEx) UpdateForAddPolicies(sec string, ptype string, rules ...[]string) error { + return nil +} + +func (w SampleWatcherEx) UpdateForRemovePolicies(sec string, ptype string, rules ...[]string) error { + return nil +} + +func TestSetWatcherEx(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + sampleWatcherEx := &SampleWatcherEx{} + err := e.SetWatcher(sampleWatcherEx) + if err != nil { + t.Fatal(err) + } + + _ = e.SavePolicy() // calls watcherEx.UpdateForSavePolicy() + _, _ = e.AddPolicy("admin", "data1", "read") // calls watcherEx.UpdateForAddPolicy() + _, _ = e.RemovePolicy("admin", "data1", "read") // calls watcherEx.UpdateForRemovePolicy() + _, _ = e.RemoveFilteredPolicy(1, "data1") // calls watcherEx.UpdateForRemoveFilteredPolicy() + _, _ = e.RemovePolicy("admin", "data1", "read") // calls watcherEx.UpdateForRemovePolicy() + _, _ = e.AddGroupingPolicy("g:admin", "data1") + _, _ = e.RemoveGroupingPolicy("g:admin", "data1") + _, _ = e.AddGroupingPolicy("g:admin", "data1") + _, _ = e.RemoveFilteredGroupingPolicy(1, "data1") + _, _ = e.AddPolicies([][]string{{"admin", "data1", "read"}, {"admin", "data2", "read"}}) // calls watcherEx.UpdateForAddPolicies() + _, _ = e.RemovePolicies([][]string{{"admin", "data1", "read"}, {"admin", "data2", "read"}}) // calls watcherEx.UpdateForRemovePolicies() +} diff --git a/watcher_test.go b/watcher_test.go index 42e93f7a1..1fe6c86b0 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -17,25 +17,75 @@ package casbin import "testing" type SampleWatcher struct { + callback func(string) } -func (w SampleWatcher) Close() { - return +func (w *SampleWatcher) Close() { } -func (w SampleWatcher) SetUpdateCallback(func(string)) error { +func (w *SampleWatcher) SetUpdateCallback(callback func(string)) error { + w.callback = callback return nil } -func (w SampleWatcher) Update() error { +func (w *SampleWatcher) Update() error { + if w.callback != nil { + w.callback("") + } return nil } func TestSetWatcher(t *testing.T) { - e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatal(err) + } + sampleWatcher := &SampleWatcher{} + err = e.SetWatcher(sampleWatcher) + if err != nil { + t.Fatal(err) + } + err = e.SavePolicy() // calls watcher.Update() + if err != nil { + t.Fatal(err) + } +} + +func TestSelfModify(t *testing.T) { + e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + if err != nil { + t.Fatal(err) + } + + sampleWatcher := &SampleWatcher{} + err = e.SetWatcher(sampleWatcher) + if err != nil { + t.Fatal(err) + } + + var called int - sampleWatcher := SampleWatcher{} - e.SetWatcher(sampleWatcher) + called = -1 + _ = e.watcher.SetUpdateCallback(func(s string) { + called = 1 + }) + _, err = e.AddPolicy("eva", "data", "read") // calls watcher.Update() + if err != nil { + t.Fatal(err) + } + if called != 1 { + t.Fatal("callback should be called") + } - e.SavePolicy() //calls watcher.Update() + called = -1 + _ = e.watcher.SetUpdateCallback(func(s string) { + called = 1 + }) + _, err = e.SelfAddPolicy("p", "p", []string{"eva", "data", "write"}) // calls watcher.Update() + if err != nil { + t.Fatal(err) + } + if called != -1 { + t.Fatal("callback should not be called") + } } diff --git a/watcher_update_test.go b/watcher_update_test.go new file mode 100644 index 000000000..081384fc8 --- /dev/null +++ b/watcher_update_test.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 casbin + +import ( + "testing" +) + +type SampleWatcherUpdatable struct { + SampleWatcher +} + +func (w SampleWatcherUpdatable) UpdateForUpdatePolicy(params ...string) error { + return nil +} + +func TestSetWatcherUpdatable(t *testing.T) { + e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") + + sampleWatcherEx := &SampleWatcherUpdatable{} + err := e.SetWatcher(sampleWatcherEx) + if err != nil { + t.Fatal(err) + } + + _ = e.SavePolicy() // calls watcherEx.UpdateForSavePolicy() + _, _ = e.UpdatePolicy([]string{"admin", "data1", "read"}, []string{"admin", "data2", "read"}) // calls watcherEx.UpdateForUpdatePolicy() +}